Conversation
WalkthroughCreates two new packages, mg-beehiiv-api and mg-beehiiv-api-members, each with package manifests, TypeScript configs, ESLint, types, source modules (clients, paginated fetchers, mappers, HTML processing), tests, and README/LICENSE/.gitignore files. Adds CLI commands and migration source runners to the migrate package that assemble Listr task pipelines to fetch Beehiiv publications/members, map data to Ghost formats, write CSV/JSON, optionally zip/upload outputs, and manage caching. Exposes public APIs and task runners for use by the CLI. Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🤖 Velo CI Failure AnalysisClassification: 🟠 SOFT FAIL
|
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (8)
packages/mg-beehiiv-api-members/README.md-1-3 (1)
1-3:⚠️ Potential issue | 🟡 MinorReplace the placeholder README before merge.
This package is set up for public consumption, but the README still has only
.... Please add the auth requirements, CLI usage, expected outputs, and any Beehiiv limitations so consumers can use it without reading the source.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/README.md` around lines 1 - 3, Replace the placeholder README in packages/mg-beehiiv-api-members/README.md with a complete user-facing document: add an "Authentication" section that lists required env vars or API token names and scopes, a "CLI usage" section with example commands and required flags/arguments for the package's CLI entrypoint, an "Expected outputs" section showing sample successful and error JSON responses from the key functions (e.g., member create/list endpoints), and a "Beehiiv limitations" section describing rate limits, pagination behavior, and any unsupported fields; include short usage examples, exit codes, and troubleshooting tips so consumers can use the package without reading the source.packages/migrate/commands/beehiiv-api.js-16-16 (1)
16-16:⚠️ Potential issue | 🟡 MinorFix misleading command description.
The description says "Migrate from beehiiv using CSV files" but this command uses the beehiiv API, not CSV files. The existing
beehiivcommand handles CSV-based migration.📝 Proposed fix
-const desc = 'Migrate from beehiiv using CSV files'; +const desc = 'Migrate from beehiiv using the beehiiv API';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api.js` at line 16, Update the misleading command description stored in the const desc in the beehiiv-api command so it accurately reflects that this command migrates from Beehiiv via the Beehiiv API (not CSV files); locate the const desc in packages/migrate/commands/beehiiv-api.js and replace the current text 'Migrate from beehiiv using CSV files' with a short, accurate description such as 'Migrate from Beehiiv using the Beehiiv API' or similar.packages/migrate/commands/beehiiv-api.js-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorRemove unused import.
The
inspectimport fromnode:utilis not used anywhere in this file.🧹 Proposed fix
-import {inspect} from 'node:util'; import {ui} from '@tryghost/pretty-cli';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api.js` at line 1, The file imports inspect from node:util but never uses it; remove the unused import statement "import {inspect} from 'node:util';" (from the top of the module) and verify there are no remaining references to inspect in functions like any helper or export in this module so ESLint/TS will stop flagging an unused import.packages/migrate/commands/beehiiv-api.js-147-150 (1)
147-150:⚠️ Potential issue | 🟡 MinorRemove dead code handling
argv.url.This code handles
argv.urlbut there is no--urloption defined in theoptionsarray. This code will never execute meaningfully.🧹 Proposed fix
- // Remove trailing slash from URL - if (argv.url && argv.url.endsWith('/')) { - argv.url = argv.url.slice(0, -1); - } - // If no publication ID is provided, list publications and exit🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api.js` around lines 147 - 150, The trailing-slash handling for argv.url is dead code because no --url option exists; remove the entire conditional block that checks and slices argv.url (the if (argv.url && argv.url.endsWith('/')) { argv.url = argv.url.slice(0, -1); }) from the migration command, or alternatively add a corresponding --url option to the options array if the URL behavior was intended—pick one and implement consistently so argv.url is never referenced without a defined option.packages/migrate/commands/beehiiv-api-members.js-124-125 (1)
124-125:⚠️ Potential issue | 🟡 MinorVerbose logging points at a non-existent result field.
The members pipeline populates
context.result.members, soinspect(context.result.data, ...)will just printundefinedon success.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api-members.js` around lines 124 - 125, The verbose log is printing context.result.data which is undefined because the members pipeline puts results in context.result.members; update the ui.log.info call (the block guarded by argv.verbose and context.result) to inspect and log context.result.members (e.g., inspect(context.result.members, false, 2)) so the actual member data is shown; ensure you reference ui.log.info, argv.verbose and context.result.members when making the change and keep the existing null-check on context.result or add a fallback if needed.packages/migrate/commands/beehiiv-api-members.js-105-110 (1)
105-110:⚠️ Potential issue | 🟡 MinorAvoid raw
console.tablein the CLI command.This currently fails the repo lint rule and bypasses the command's normal UI/logging path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api-members.js` around lines 105 - 110, The CLI code uses raw console.table on getPubs which violates lint and bypasses the command's logging/UI; replace the console.table call with the command's logger/UI output (e.g., use the command context logger or existing processLogger) and render the same rows by formatting getPubs.map(...) into a string or using the repo's table helper before calling logger.info/log or the command's print function; update the invocation around getPubs and the anonymous map so it passes the formatted string to the logger instead of calling console.table.packages/mg-beehiiv-api-members/src/test/fetch.test.ts-179-182 (1)
179-182:⚠️ Potential issue | 🟡 MinorMock
discover()with the real publication payload.
fetchTasks()callsdiscover(), anddiscover()readsresponse.json().data.stats.active_subscriptions. These stubs returntotal_resultsinstead, soestimatedPagesbecomesNaNand the pagination/progress path is never actually exercised.Also applies to: 201-204, 245-248, 276-279, 309-312
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/src/test/fetch.test.ts` around lines 179 - 182, The mocked fetch responses used in the tests return an object shaped for list endpoints ({ total_results }) but discover() expects the publication payload and reads response.json().data.stats.active_subscriptions; update the fetchMock.mock.mockImplementationOnce stubs used by tests that exercise fetchTasks() (and indirectly discover()) to return a JSON shape matching the real publication payload—i.e. response.json() should resolve to an object with a data.stats.active_subscriptions numeric field (and any other fields discover() needs such as has_more/page indicators) so estimatedPages is computed correctly; apply the same change to the other test stubs referenced (the blocks at the other locations listed) to ensure pagination/progress paths are exercised.packages/mg-beehiiv-api/src/lib/process.ts-483-499 (1)
483-499:⚠️ Potential issue | 🟡 MinorGuard against undefined values to prevent runtime errors.
Several potential issues:
- Line 486:
firstImgSrccould beundefined(fromattr('src')), causing.split()to throw.- Line 489: If URLs don't contain
/uploads/asset/,split()[1]returnsundefined, andundefined === undefinedevaluates totrue, potentially removing non-duplicate images.Proposed fix with safety checks
- // Both images usually end in the same way, so we can split the URL and compare the last part - const firstImageSplit = firstImgSrc.split('/uploads/asset/'); - const featureImageSplit = featureSrc.split('/uploads/asset/'); - - if (firstImageSplit[1] === featureImageSplit[1]) { - $html(theElementItself).remove(); - } - - if (featureSrc.length > 0 && firstImgSrc) { + if (firstImgSrc && featureSrc) { + // Both images usually end in the same way, so we can split the URL and compare the last part + const firstImageSplit = firstImgSrc.split('/uploads/asset/'); + const featureImageSplit = featureSrc.split('/uploads/asset/'); + + if (firstImageSplit[1] && featureImageSplit[1] && firstImageSplit[1] === featureImageSplit[1]) { + $html(theElementItself).remove(); + return $html.html(); + } + let normalizedFirstSrc = firstImgSrc.replace('fit=scale-down,format=auto,onerror=redirect,quality=80', 'quality=100'); if (featureSrc === normalizedFirstSrc) { $html(theElementItself).remove(); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/process.ts` around lines 483 - 499, Guard against undefined and missing segments before splitting and comparing: ensure firstImgSrc is truthy before calling split or replace, and verify both firstImgSrc and featureSrc contain '/uploads/asset/' (or that split()[1] exists) before comparing their [1] parts; also check featureSrc is non-empty before normalizing and comparing to normalizedFirstSrc. Specifically, update the logic around variables firstImgSrc, firstImageSplit, featureImageSplit and normalizedFirstSrc in the block using $html(theElementItself) so you only call .split(), .replace(), or access [1] when the source strings are defined and include the expected substring, and bail out (skip removal) if those guards fail.
🧹 Nitpick comments (8)
packages/mg-beehiiv-api/tsconfig.json (2)
110-110:src/index.jsinincludeis problematic.Two issues with this entry:
- The glob
"src/**/*"already covers all files insrc/, making"src/index.js"redundant if it's a TypeScript file.- If
src/index.jsis actually a JavaScript file, it won't be processed becauseallowJsis not enabled (line 48 is commented out).If this package uses TypeScript throughout, remove the redundant entry. If there's genuinely a JS entry point, enable
allowJs.♻️ Suggested fix (assuming TypeScript only)
- "include": ["src/**/*", "src/index.js"] + "include": ["src/**/*"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/tsconfig.json` at line 110, The tsconfig's "include" currently lists "src/**/*" and the redundant "src/index.js"; remove "src/index.js" from the "include" array if the codebase is TypeScript-only (so only "src/**/*" remains), otherwise if there truly is a JavaScript entry point enable "allowJs": true (the commented out "allowJs" setting around line 48) so the JS file will be picked up—apply the appropriate change in the tsconfig.json.
86-87:noImplicitAnyis redundant withstrictmode.When
strict: trueis enabled, it implicitly enablesnoImplicitAny(among other strict checks). The explicit setting is harmless but unnecessary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/tsconfig.json` around lines 86 - 87, The tsconfig.json contains a redundant "noImplicitAny" entry because "strict": true already enables it; remove the explicit "noImplicitAny" property (or alternatively set it to identical value via a comment if you prefer explicitness) so only "strict": true remains as the single source of truth for strict checks; locate the JSON object containing the "strict" and "noImplicitAny" keys and delete the "noImplicitAny" line.packages/mg-beehiiv-api/src/lib/client.ts (1)
1-18: Avoid maintaining a second publications client here.
packages/mg-beehiiv-api/src/lib/list-pubs.tsalready wraps the authenticated publications request on top ofauthedClient, andpackages/mg-beehiiv-api/src/index.tsdoes not even export thisclient. Keeping both paths means the shipped code and the tested code can drift—they already differ on theexpand[]=statsquery parameter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/client.ts` around lines 1 - 18, The file defines a duplicate async function client that issues a GET to /publications; instead of keeping this second publications client, remove or replace its usage and delegate to the existing authedClient used by list-pubs.ts to avoid divergence (ensure the same query params such as expand[]=stats are preserved). Locate the client function in this file and either delete it or refactor callers to call the shared authedClient wrapper (the same helper used by packages/mg-beehiiv-api/src/lib/list-pubs.ts) so the exported surface matches index.ts and the requests remain identical.packages/mg-beehiiv-api-members/src/index.ts (1)
5-9: Consider using named exports for utilities.Same feedback as for
packages/mg-beehiiv-api/src/index.ts— these are utility functions that would be better served by named exports per coding guidelines.♻️ Suggested refactor using named exports
import {listPublications} from './lib/list-pubs.js'; import {fetchTasks} from './lib/fetch.js'; import {mapMembersTasks} from './lib/mapper.js'; -export default { - listPublications, - fetchTasks, - mapMembersTasks -}; +export { + listPublications, + fetchTasks, + mapMembersTasks +};As per coding guidelines: "Named exports should be used for utilities, and default exports for main functionality (classes)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/src/index.ts` around lines 5 - 9, Current default export is exporting utility functions as a single default object; change this to named exports by exporting listPublications, fetchTasks, and mapMembersTasks individually (remove the default-exported object) so callers can import them as named imports; update any internal references or import sites to use the named imports if necessary (search for import of the default from this module and switch to { listPublications, fetchTasks, mapMembersTasks }).packages/mg-beehiiv-api/src/index.ts (1)
5-9: Consider using named exports for utilities.Per coding guidelines, named exports should be used for utilities in index files, while default exports are reserved for main functionality (classes). Since
listPublications,fetchTasks, andmapPostsTasksare utility functions, named exports would be more appropriate.♻️ Suggested refactor using named exports
import {listPublications} from './lib/list-pubs.js'; import {mapPostsTasks} from './lib/mapper.js'; import {fetchTasks} from './lib/fetch.js'; -export default { - listPublications, - fetchTasks, - mapPostsTasks -}; +export { + listPublications, + fetchTasks, + mapPostsTasks +};Note: This would require updating consumers (e.g.,
packages/migrate/sources/beehiiv-api.jsandpackages/migrate/commands/beehiiv-api.js) to use named imports instead of default import.As per coding guidelines: "Named exports should be used for utilities, and default exports for main functionality (classes)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/index.ts` around lines 5 - 9, Replace the default export object with named exports for the utility functions: export the functions listPublications, fetchTasks, and mapPostsTasks as named exports (e.g., export { listPublications, fetchTasks, mapPostsTasks }) and update any consumers (such as migrate/sources/beehiiv-api.js and migrate/commands/beehiiv-api.js) to import those functions by name instead of using the default import.packages/mg-beehiiv-api-members/src/lib/list-pubs.ts (1)
9-11: Consider using@tryghost/errorsfor structured error handling.Per coding guidelines, errors should use
@tryghost/errorswith message and context properties for better debugging and error tracking.♻️ Suggested refactor using `@tryghost/errors`
+import errors from '@tryghost/errors'; import {authedClient} from './fetch.js'; const listPublications = async (apiKey: string) => { const url = new URL(`https://api.beehiiv.com/v2/publications`); url.searchParams.append('expand[]', 'stats'); const response = await authedClient(apiKey, url); if (!response.ok) { - throw new Error(`Request failed: ${response.status} ${response.statusText}`); + throw new errors.InternalServerError({ + message: `Request failed: ${response.status} ${response.statusText}`, + context: { + url: url.toString(), + status: response.status, + statusText: response.statusText + } + }); } const data = await response.json(); return data.data; };As per coding guidelines: "Use
@tryghost/errorsfor structured error handling with message and context properties."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/src/lib/list-pubs.ts` around lines 9 - 11, The current plain Error thrown when response.ok is false should be replaced with an `@tryghost/errors` structured error: import errors from '@tryghost/errors' and throw an instance that includes a descriptive message and a context object (e.g., {status: response.status, statusText: response.statusText, url: <request url or variable>}) in the list-pubs code path where response is checked; update the code around response.ok in the listPubs (or the function that performs the fetch) to construct and throw that `@tryghost/errors` error instead of new Error(...) so logs and error handling get structured message and context.packages/mg-beehiiv-api/src/lib/process.ts (2)
3-5: Remove unused imports.
sanitizeHtml(line 3) andimageCard(line 5) are imported but only referenced in commented-out code. Remove these to avoid confusion and dead imports.Proposed fix
import * as cheerio from 'cheerio'; import _ from 'lodash'; -import sanitizeHtml from 'sanitize-html'; import SimpleDom from 'simple-dom'; -import imageCard from '@tryghost/kg-default-cards/lib/cards/image.js'; import embedCard from '@tryghost/kg-default-cards/lib/cards/embed.js'; import bookmarkCard from '@tryghost/kg-default-cards/lib/cards/bookmark.js';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/process.ts` around lines 3 - 5, Remove the dead imports sanitizeHtml and imageCard from the top of the file—keep only the actually used SimpleDom import; update the import list in packages/mg-beehiiv-api/src/lib/process.ts by deleting the sanitizeHtml and imageCard import statements (they are only referenced in commented-out code) so there are no unused imports left.
160-430: Remove or relocate commented-out code.This block contains ~270 lines of commented-out code, which is a significant maintenance burden. If this code is experimental or for future use, consider:
- Removing it entirely (it's preserved in version control history)
- Moving it to a separate file or documentation if it serves as reference
- Creating a tracking issue if it represents planned functionality
Keeping large blocks of dead code makes the file harder to read and maintain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/process.ts` around lines 160 - 430, The large ~270-line commented block in process.ts (inside the HTML cleaning logic that manipulates $html, options, postData, imageCard, embedCard, bookmarkCard, serializer, getYouTubeID, etc.) should be removed or moved out of the file: either delete it (history preserves it), extract it into a dedicated reference file (e.g., a legacy or experimental helpers file) or add it to a design doc/issue tracking planned features; update imports/usages if you relocate helpers and ensure the active processing functions (the HTML-cleanup routine surrounding $html and related handlers) remain clean and focused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/mg-beehiiv-api-members/.eslintrc.cjs`:
- Around line 10-12: The config currently disables the enforced `@tryghost/errors`
rules; revert that by removing the two overrides
('ghost/ghost-custom/no-native-error' and
'ghost/ghost-custom/ghost-error-usage') from .eslintrc.cjs and re-enable
linting. In code, replace native Error usages in src/lib/list-pubs.ts and
src/lib/fetch.ts: import and throw the appropriate `@tryghost/errors` class (e.g.,
GhostError or a more specific error like BadRequestError/NotFoundError) instead
of new Error(...), and ensure any error construction/messaging matches the
chosen `@tryghost/errors` API so the linter passes. Ensure imports reference
`@tryghost/errors` at the top of each file and update thrown instances to use
those classes.
In `@packages/mg-beehiiv-api-members/src/lib/fetch.ts`:
- Around line 19-20: Replace raw Error throws in the response.ok checks with a
structured error from `@tryghost/errors`: import the package and throw a
`@tryghost/errors` error (e.g. new errors.InternalServerError or a suitable
subclass) that includes a descriptive message and a context object containing
pubId, cursor, cursorIndex and the HTTP status/statusText; update both places
where the code currently does throw new Error(...) (the response.ok check around
the first failure and the second occurrence at the later response check) so the
thrown error uses the `@tryghost/errors` API and attaches the request context for
debuggability.
In `@packages/mg-beehiiv-api-members/src/types.d.ts`:
- Around line 1-45: The types are currently declared as ambient globals and must
be exported to avoid polluting consumers; change the type declarations
(BeehiivSubscription, BeehiivPublicationResponse, BeehiivSubscriptionsResponse,
GhostMemberObject, MappedMembers) to exported types (e.g., export type ...) and
then update mapper.ts and fetch.ts to import the needed types instead of relying
on globals; alternatively move the two declare module shims into a separate
ambient declaration file (e.g., a .d.ts) and keep only exported type definitions
in this file so consumers only receive explicit exports.
In `@packages/mg-beehiiv-api/.eslintrc.cjs`:
- Around line 10-12: Remove the three disabled ESLint rules
('ghost/ghost-custom/no-native-errors', 'ghost/ghost-custom/no-native-error',
'ghost/ghost-custom/ghost-error-usage') from .eslintrc.cjs so the Ghost error
linting is enforced, then update the native throw in src/lib/client.ts (the
"throw new Error(...)" at the top of the file) to use the structured error types
from `@tryghost/errors`: import the appropriate error class from `@tryghost/errors`
(e.g., errors.GhostError or the most semantically appropriate errors.* class)
and throw that instead with the same message/metadata so the package conforms to
the project's structured error handling.
In `@packages/mg-beehiiv-api/README.md`:
- Around line 1-3: The README.md is a placeholder and must be expanded into a
full package README: add a brief package description and purpose, installation
instructions (npm/yarn/pnpm commands), usage examples including CLI commands and
code snippets showing how to call the package, API documentation listing
exported functions/endpoints and expected parameters, configuration details for
Beehiiv credentials and env vars, examples of input/output formats and sample
responses, and a troubleshooting/FAQ section; update the top-level README.md in
this package (packages/mg-beehiiv-api/README.md) to include these sections and
concrete examples matching the package's exported API and configuration keys.
In `@packages/mg-beehiiv-api/src/lib/client.ts`:
- Around line 11-12: Replace the generic throw new Error in the response.ok
check with a Ghost structured error: import errors from '@tryghost/errors' (or
ensure it's imported) and throw new errors.InternalServerError({message:
`Beehiiv API request failed: ${response.statusText}`, context: {status:
response.status, statusText: response.statusText, url: url.toString()}}) so the
check around response.ok (the existing if (!response.ok) block) includes status,
statusText and the request URL in the context for consistent migration
reporting.
In `@packages/mg-beehiiv-api/src/lib/fetch.ts`:
- Around line 18-19: Replace plain Error throws that use response data with
structured `@tryghost/errors.InternalServerError` instances and pass the local
pubId and page in the context; specifically, import errors from
'@tryghost/errors' if not already present and change the two throw sites that
currently do throw new Error(`Request failed: ${response.status}
${response.statusText}`) to throw new errors.InternalServerError({ message:
`Request failed: ${response.status} ${response.statusText}`, context: {pubId,
page} }); do this for both occurrences so the error includes diagnostic context
when propagated.
In `@packages/mg-beehiiv-api/src/lib/list-pubs.ts`:
- Around line 9-10: Replace the raw thrown Error in listPublications (in
packages/mg-beehiiv-api/src/lib/list-pubs.ts) with a structured `@tryghost/errors`
instance: import errors from '@tryghost/errors' at the top and change the throw
to throw new errors.InternalServerError({message: `Request failed:
${response.status} ${response.statusText}`}) so the HTTP failure uses the
codebase's standardized error type and includes the same status/text message.
In `@packages/mg-beehiiv-api/src/lib/mapper.ts`:
- Around line 11-13: The mapping in mapper.ts incorrectly sets updated_at from
postData.created; change the mapping for updated_at to use the source update
timestamp (e.g., postData.updated or postData.modified) when available, and only
fall back to omitting updated_at (or leaving it undefined/null) if the source
does not provide an update field; update the mapping that assigns published_at,
updated_at, created_at (references: postData.publish_date,
postData.updated/postData.modified, postData.created) so updated_at reflects the
real source update time instead of created.
In `@packages/mg-beehiiv-api/src/test/process.test.ts`:
- Around line 223-230: The test currently asserts that
removeDuplicateFeatureImage removes an image when featureSrc is empty, which
locks in a bug; update the test in process.test.ts to expect the image to be
preserved when featureSrc === '' by changing the assertion to check that result
includes the original <img> (e.g., assert.ok(result.includes('<img') or
assert.strictEqual(result, html) or check the src remains unchanged) and ensure
the test references the removeDuplicateFeatureImage function and the existing
html/featureSrc variables so it verifies preservation rather than removal.
In `@packages/mg-beehiiv-api/src/types.d.ts`:
- Around line 1-76: Add explicit exports for the top-level type declarations to
avoid creating ambient globals: update each type declaration
(beehiivPostDataObject, tagsObject, authorsObject, mappedDataObject) to be
exported (e.g., export type ...) so they are module-scoped and available to
consumers; keep or adjust the existing declare module lines as needed but do not
leave the plain types unexported—alternatively move these types into a
module-scoped file and re-export them from your package index so the published
"types" entry no longer pollutes the global scope.
In `@packages/migrate/commands/beehiiv-api-members.js`:
- Around line 96-115: Move the no---id publication listing branch inside the
existing try block within run() so any rejection from
mgBeehiivApiMembers.listPublications(argv.key) is caught; replace
process.exit(0) with a return to exit the function gracefully, and ensure errors
use ui.log.error (preserving the current table/log output and the ui.log.warn
message) so listPublications() failures are handled by the surrounding catch.
In `@packages/migrate/commands/beehiiv-api.js`:
- Around line 180-183: The verbose flag check (argv.verbose) is used but the
corresponding option in the options array is commented out, which breaks verbose
behavior and also leaves AssetScraper expecting options.verbose; restore the CLI
option by uncommenting the verbose option declaration in the options array so
argv.verbose and options.verbose are defined, or alternatively remove the
argv.verbose check and any references to options.verbose in the AssetScraper
task in sources/beehiiv-api.js—specifically ensure the CLI options array
includes the verbose option name/flag so argv.verbose and options.verbose are
available to the code paths that log the detailed inspect output.
In `@packages/migrate/sources/beehiiv-api-members.js`:
- Around line 154-155: The two error pushes using ctx.errors.push('Failed to
write and upload output file', error) and ctx.errors.push('Failed to clear
cache', error) should be changed to the structured-object form used elsewhere;
replace those calls so they push a single object like ctx.errors.push({message:
'Failed to write and upload output file', error}) and ctx.errors.push({message:
'Failed to clear cache', error}) so the message/error pairing is preserved and
consistent with other uses of ctx.errors.push in this module.
In `@packages/migrate/sources/beehiiv-api.js`:
- Around line 72-129: Re-enable the post-processing tasks that run after JSON
formatting: restore the link map build (ctx.linkFixer.buildMap), the link-fixing
task (ctx.linkFixer.fix) and the HTML->Lexical conversion
(mgHtmlLexical.convert) into the task list surrounding the toGhostJSON and asset
scraping steps so exported data has links and HTML transformed before writing;
ensure the tasks use the same error handling pattern (push to ctx.errors and
rethrow) and return their task runners (makeTaskRunner) like the other tasks so
the pipeline executes them in order.
- Around line 61-68: The catch blocks push heterogeneous error entries into
ctx.errors; update each ctx.errors.push call (the ones inside the async task
that calls mgBeehiiv.mapPostsTasks and the other catches that log "Failed to
format data as Ghost JSON" and "Failed to write Ghost import JSON File") to use
the consistent structured object form used elsewhere: ctx.errors.push({ message:
'...', error }); — locate the catches around the async task that returns
makeTaskRunner(tasks, {...options}) and the other catch sites and replace the
two-argument pushes with a single object containing message and error so all
entries match the pattern at the earlier line that uses {message, error}.
---
Minor comments:
In `@packages/mg-beehiiv-api-members/README.md`:
- Around line 1-3: Replace the placeholder README in
packages/mg-beehiiv-api-members/README.md with a complete user-facing document:
add an "Authentication" section that lists required env vars or API token names
and scopes, a "CLI usage" section with example commands and required
flags/arguments for the package's CLI entrypoint, an "Expected outputs" section
showing sample successful and error JSON responses from the key functions (e.g.,
member create/list endpoints), and a "Beehiiv limitations" section describing
rate limits, pagination behavior, and any unsupported fields; include short
usage examples, exit codes, and troubleshooting tips so consumers can use the
package without reading the source.
In `@packages/mg-beehiiv-api-members/src/test/fetch.test.ts`:
- Around line 179-182: The mocked fetch responses used in the tests return an
object shaped for list endpoints ({ total_results }) but discover() expects the
publication payload and reads response.json().data.stats.active_subscriptions;
update the fetchMock.mock.mockImplementationOnce stubs used by tests that
exercise fetchTasks() (and indirectly discover()) to return a JSON shape
matching the real publication payload—i.e. response.json() should resolve to an
object with a data.stats.active_subscriptions numeric field (and any other
fields discover() needs such as has_more/page indicators) so estimatedPages is
computed correctly; apply the same change to the other test stubs referenced
(the blocks at the other locations listed) to ensure pagination/progress paths
are exercised.
In `@packages/mg-beehiiv-api/src/lib/process.ts`:
- Around line 483-499: Guard against undefined and missing segments before
splitting and comparing: ensure firstImgSrc is truthy before calling split or
replace, and verify both firstImgSrc and featureSrc contain '/uploads/asset/'
(or that split()[1] exists) before comparing their [1] parts; also check
featureSrc is non-empty before normalizing and comparing to normalizedFirstSrc.
Specifically, update the logic around variables firstImgSrc, firstImageSplit,
featureImageSplit and normalizedFirstSrc in the block using
$html(theElementItself) so you only call .split(), .replace(), or access [1]
when the source strings are defined and include the expected substring, and bail
out (skip removal) if those guards fail.
In `@packages/migrate/commands/beehiiv-api-members.js`:
- Around line 124-125: The verbose log is printing context.result.data which is
undefined because the members pipeline puts results in context.result.members;
update the ui.log.info call (the block guarded by argv.verbose and
context.result) to inspect and log context.result.members (e.g.,
inspect(context.result.members, false, 2)) so the actual member data is shown;
ensure you reference ui.log.info, argv.verbose and context.result.members when
making the change and keep the existing null-check on context.result or add a
fallback if needed.
- Around line 105-110: The CLI code uses raw console.table on getPubs which
violates lint and bypasses the command's logging/UI; replace the console.table
call with the command's logger/UI output (e.g., use the command context logger
or existing processLogger) and render the same rows by formatting
getPubs.map(...) into a string or using the repo's table helper before calling
logger.info/log or the command's print function; update the invocation around
getPubs and the anonymous map so it passes the formatted string to the logger
instead of calling console.table.
In `@packages/migrate/commands/beehiiv-api.js`:
- Line 16: Update the misleading command description stored in the const desc in
the beehiiv-api command so it accurately reflects that this command migrates
from Beehiiv via the Beehiiv API (not CSV files); locate the const desc in
packages/migrate/commands/beehiiv-api.js and replace the current text 'Migrate
from beehiiv using CSV files' with a short, accurate description such as
'Migrate from Beehiiv using the Beehiiv API' or similar.
- Line 1: The file imports inspect from node:util but never uses it; remove the
unused import statement "import {inspect} from 'node:util';" (from the top of
the module) and verify there are no remaining references to inspect in functions
like any helper or export in this module so ESLint/TS will stop flagging an
unused import.
- Around line 147-150: The trailing-slash handling for argv.url is dead code
because no --url option exists; remove the entire conditional block that checks
and slices argv.url (the if (argv.url && argv.url.endsWith('/')) { argv.url =
argv.url.slice(0, -1); }) from the migration command, or alternatively add a
corresponding --url option to the options array if the URL behavior was
intended—pick one and implement consistently so argv.url is never referenced
without a defined option.
---
Nitpick comments:
In `@packages/mg-beehiiv-api-members/src/index.ts`:
- Around line 5-9: Current default export is exporting utility functions as a
single default object; change this to named exports by exporting
listPublications, fetchTasks, and mapMembersTasks individually (remove the
default-exported object) so callers can import them as named imports; update any
internal references or import sites to use the named imports if necessary
(search for import of the default from this module and switch to {
listPublications, fetchTasks, mapMembersTasks }).
In `@packages/mg-beehiiv-api-members/src/lib/list-pubs.ts`:
- Around line 9-11: The current plain Error thrown when response.ok is false
should be replaced with an `@tryghost/errors` structured error: import errors from
'@tryghost/errors' and throw an instance that includes a descriptive message and
a context object (e.g., {status: response.status, statusText:
response.statusText, url: <request url or variable>}) in the list-pubs code path
where response is checked; update the code around response.ok in the listPubs
(or the function that performs the fetch) to construct and throw that
`@tryghost/errors` error instead of new Error(...) so logs and error handling get
structured message and context.
In `@packages/mg-beehiiv-api/src/index.ts`:
- Around line 5-9: Replace the default export object with named exports for the
utility functions: export the functions listPublications, fetchTasks, and
mapPostsTasks as named exports (e.g., export { listPublications, fetchTasks,
mapPostsTasks }) and update any consumers (such as
migrate/sources/beehiiv-api.js and migrate/commands/beehiiv-api.js) to import
those functions by name instead of using the default import.
In `@packages/mg-beehiiv-api/src/lib/client.ts`:
- Around line 1-18: The file defines a duplicate async function client that
issues a GET to /publications; instead of keeping this second publications
client, remove or replace its usage and delegate to the existing authedClient
used by list-pubs.ts to avoid divergence (ensure the same query params such as
expand[]=stats are preserved). Locate the client function in this file and
either delete it or refactor callers to call the shared authedClient wrapper
(the same helper used by packages/mg-beehiiv-api/src/lib/list-pubs.ts) so the
exported surface matches index.ts and the requests remain identical.
In `@packages/mg-beehiiv-api/src/lib/process.ts`:
- Around line 3-5: Remove the dead imports sanitizeHtml and imageCard from the
top of the file—keep only the actually used SimpleDom import; update the import
list in packages/mg-beehiiv-api/src/lib/process.ts by deleting the sanitizeHtml
and imageCard import statements (they are only referenced in commented-out code)
so there are no unused imports left.
- Around line 160-430: The large ~270-line commented block in process.ts (inside
the HTML cleaning logic that manipulates $html, options, postData, imageCard,
embedCard, bookmarkCard, serializer, getYouTubeID, etc.) should be removed or
moved out of the file: either delete it (history preserves it), extract it into
a dedicated reference file (e.g., a legacy or experimental helpers file) or add
it to a design doc/issue tracking planned features; update imports/usages if you
relocate helpers and ensure the active processing functions (the HTML-cleanup
routine surrounding $html and related handlers) remain clean and focused.
In `@packages/mg-beehiiv-api/tsconfig.json`:
- Line 110: The tsconfig's "include" currently lists "src/**/*" and the
redundant "src/index.js"; remove "src/index.js" from the "include" array if the
codebase is TypeScript-only (so only "src/**/*" remains), otherwise if there
truly is a JavaScript entry point enable "allowJs": true (the commented out
"allowJs" setting around line 48) so the JS file will be picked up—apply the
appropriate change in the tsconfig.json.
- Around line 86-87: The tsconfig.json contains a redundant "noImplicitAny"
entry because "strict": true already enables it; remove the explicit
"noImplicitAny" property (or alternatively set it to identical value via a
comment if you prefer explicitness) so only "strict": true remains as the single
source of truth for strict checks; locate the JSON object containing the
"strict" and "noImplicitAny" keys and delete the "noImplicitAny" line.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: be04ed1d-a0da-4510-90ed-e88179c30dbc
⛔ Files ignored due to path filters (2)
packages/mg-beehiiv-api/src/test/fixtures/posts.csvis excluded by!**/*.csvyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (37)
packages/mg-beehiiv-api-members/.eslintrc.cjspackages/mg-beehiiv-api-members/README.mdpackages/mg-beehiiv-api-members/package.jsonpackages/mg-beehiiv-api-members/src/index.tspackages/mg-beehiiv-api-members/src/lib/fetch.tspackages/mg-beehiiv-api-members/src/lib/list-pubs.tspackages/mg-beehiiv-api-members/src/lib/mapper.tspackages/mg-beehiiv-api-members/src/test/fetch.test.tspackages/mg-beehiiv-api-members/src/test/index.test.tspackages/mg-beehiiv-api-members/src/test/mapper.test.tspackages/mg-beehiiv-api-members/src/types.d.tspackages/mg-beehiiv-api-members/tsconfig.jsonpackages/mg-beehiiv-api/.eslintrc.cjspackages/mg-beehiiv-api/.gitignorepackages/mg-beehiiv-api/LICENSEpackages/mg-beehiiv-api/README.mdpackages/mg-beehiiv-api/package.jsonpackages/mg-beehiiv-api/src/index.tspackages/mg-beehiiv-api/src/lib/client.tspackages/mg-beehiiv-api/src/lib/fetch.tspackages/mg-beehiiv-api/src/lib/list-pubs.tspackages/mg-beehiiv-api/src/lib/mapper.tspackages/mg-beehiiv-api/src/lib/process.tspackages/mg-beehiiv-api/src/test/client.test.tspackages/mg-beehiiv-api/src/test/fetch.test.tspackages/mg-beehiiv-api/src/test/index.test.tspackages/mg-beehiiv-api/src/test/list-pubs.test.tspackages/mg-beehiiv-api/src/test/mapper.test.tspackages/mg-beehiiv-api/src/test/process.test.tspackages/mg-beehiiv-api/src/types.d.tspackages/mg-beehiiv-api/tsconfig.jsonpackages/migrate/bin/cli.jspackages/migrate/commands/beehiiv-api-members.jspackages/migrate/commands/beehiiv-api.jspackages/migrate/package.jsonpackages/migrate/sources/beehiiv-api-members.jspackages/migrate/sources/beehiiv-api.js
| # Migrate beehiiv API | ||
|
|
||
| ... |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Complete the README documentation.
The README is currently a placeholder and needs comprehensive documentation before this PR can be merged. A migration package should include:
- Package description and purpose
- Installation instructions
- Usage examples and CLI commands
- API documentation
- Configuration options (e.g., Beehiiv API credentials)
- Input/output format examples
- Troubleshooting guide
Would you like me to generate a comprehensive README template based on the package's functionality described in the AI summary?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api/README.md` around lines 1 - 3, The README.md is a
placeholder and must be expanded into a full package README: add a brief package
description and purpose, installation instructions (npm/yarn/pnpm commands),
usage examples including CLI commands and code snippets showing how to call the
package, API documentation listing exported functions/endpoints and expected
parameters, configuration details for Beehiiv credentials and env vars, examples
of input/output formats and sample responses, and a troubleshooting/FAQ section;
update the top-level README.md in this package
(packages/mg-beehiiv-api/README.md) to include these sections and concrete
examples matching the package's exported API and configuration keys.
afe85f5 to
94680d6
Compare
🤖 Velo CI Failure AnalysisClassification: 🟠 SOFT FAIL
|
94680d6 to
ce0b800
Compare
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (2)
packages/migrate/commands/beehiiv-api-members.js (1)
96-115:⚠️ Potential issue | 🟠 MajorKeep the no-
--idbranch inside thetryand return instead of hard-exiting.
mgBeehiivApiMembers.listPublications()still executes before the catch block, so discovery failures bypassui.log.error(), andprocess.exit(0)short-circuits the normal command return path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api-members.js` around lines 96 - 115, Move the "no --id" branch so that the call to mgBeehiivApiMembers.listPublications(...) happens inside the existing try block (so failures are caught by the catch), and replace process.exit(0) with a plain return to allow the command to return normally; keep the same ui.log.error and ui.log.warn messages but ensure any thrown errors from listPublications are handled by the catch block rather than escaping it.packages/mg-beehiiv-api/src/lib/mapper.ts (1)
11-13:⚠️ Potential issue | 🟠 MajorDon't derive
updated_atfromcreated.This makes every edited post look untouched after import. Map Beehiiv’s real update timestamp when it exists, or omit
updated_atinstead of copying the create time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/mapper.ts` around lines 11 - 13, The mapping currently sets updated_at from postData.created which hides edits; in mapper.ts update the mapping for updated_at to use Beehiiv's real update timestamp when present (e.g., check postData.updated, postData.updated_at, or whatever the API field is) and convert it to a Date (new Date(postData.<updateField> * 1000)); if no update timestamp exists, omit updated_at (or set it to undefined) instead of copying created_at. Locate the mapping that sets published_at, updated_at, created_at and replace the updated_at assignment accordingly.
🧹 Nitpick comments (1)
packages/mg-beehiiv-api/src/lib/fetch.ts (1)
68-79: Date filtering is inefficient—runs on entire array each page.The
postsAfterandpostsBeforefilters re-process the entire accumulatedctx.result.postsarray on every page fetch. Move these filters outside the pagination loop (after all pages are fetched) to avoid O(n²) behavior.♻️ Suggested refactor
export const fetchTasks = async (options: any, ctx: any) => { const numberOfPosts = await discover(options.key, options.id, API_LIMIT); const totalPages = Math.ceil(numberOfPosts / API_LIMIT); let tasks = []; for (let page = 1; page <= totalPages; page++) { tasks.push({ title: `Fetching posts page ${page} of ${totalPages}`, task: async (_: any, task: any) => { try { let response = await cachedFetch({fileCache: ctx.fileCache ,key: options.key, pubId: options.id, page: page}); ctx.result.posts = ctx.result.posts.concat(response.data); - - if (options.postsAfter) { - const afterTimestamp = new Date(options.postsAfter).getTime() / 1000; - ctx.result.posts = ctx.result.posts.filter( - (post: any) => post.publish_date >= afterTimestamp - ); - } - if (options.postsBefore) { - const beforeTimestamp = new Date(options.postsBefore).getTime() / 1000; - ctx.result.posts = ctx.result.posts.filter( - (post: any) => post.publish_date <= beforeTimestamp - ); - } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); task.output = errorMessage; throw error; } } }); } + // Add a final task to filter by date range + if (options.postsAfter || options.postsBefore) { + tasks.push({ + title: 'Filtering posts by date range', + task: async () => { + if (options.postsAfter) { + const afterTimestamp = new Date(options.postsAfter).getTime() / 1000; + ctx.result.posts = ctx.result.posts.filter( + (post: any) => post.publish_date >= afterTimestamp + ); + } + if (options.postsBefore) { + const beforeTimestamp = new Date(options.postsBefore).getTime() / 1000; + ctx.result.posts = ctx.result.posts.filter( + (post: any) => post.publish_date <= beforeTimestamp + ); + } + } + }); + } return tasks; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/fetch.ts` around lines 68 - 79, The date filters (options.postsAfter and options.postsBefore) are currently applied to ctx.result.posts inside the pagination loop, causing repeated full-array scans; move the filtering logic out of the loop so you fetch and append all pages first (the code that populates ctx.result.posts), then after pagination completes apply a single filter pass using options.postsAfter and options.postsBefore (convert to timestamps once, e.g. new Date(...).getTime()/1000) to produce the final ctx.result.posts; update/remove the in-loop blocks that reference ctx.result.posts.filter and ensure you only compare post.publish_date to the computed afterTimestamp/beforeTimestamp once after all pages are aggregated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/mg-beehiiv-api-members/src/index.ts`:
- Around line 1-9: Replace the default object export with named exports for the
utility functions; instead of exporting a default object containing
listPublications, fetchTasks, and mapMembersTasks, export each symbol by name
(listPublications, fetchTasks, mapMembersTasks) so consumers can import them
directly (e.g., import { listPublications } from ...). Update the package
entrypoint to use named exports for the listed functions and remove the default
export.
In `@packages/mg-beehiiv-api-members/src/lib/list-pubs.ts`:
- Around line 9-10: Replace the bare Error thrown when the fetch response is not
ok with the repository-standard structured error: use errors.InternalServerError
and pass the message string like `Request failed: ${response.status}
${response.statusText}`; update the throw in the response.ok check (the block
that currently does `throw new Error(...)` in list-pubs.ts) to `throw new
errors.InternalServerError({ message: ... })` and ensure the `errors` symbol
from `@tryghost/errors` is imported where `response` is checked.
In `@packages/mg-beehiiv-api/src/lib/fetch.ts`:
- Line 12: The discover function currently accepts a limit parameter but ignores
it (hardcoded to '1'); update the implementation of discover to use the provided
limit (e.g., convert limit to string where the query currently hardcodes '1')
and ensure callers of discover (the call site that currently passes a value)
continue to pass the intended limit value; alternatively, if the parameter is
unnecessary, remove limit from discover's signature and remove/update all call
sites accordingly—prefer using the parameter by replacing the hardcoded '1' with
limit.toString() in the discover implementation.
In `@packages/mg-beehiiv-api/src/lib/mapper.ts`:
- Around line 25-40: The code sets mappedData.data.feature_image before calling
processHTML, but never runs the removeDuplicateFeatureImage pass afterward, so
hero images duplicated as the first body image remain; after you compute
mappedData.data.html via processHTML({ post: mappedData, options }) call
removeDuplicateFeatureImage(mappedData) (or
removeDuplicateFeatureImage(mappedData.data.html, mappedData.data.feature_image)
depending on that helper's signature) and assign its result back into
mappedData.data.html (or update mappedData accordingly) so the duplicate feature
image is stripped from the rendered HTML.
In `@packages/mg-beehiiv-api/src/lib/process.ts`:
- Around line 154-167: The iframe selector only matches src starting with
"https://youtube" so standard "https://www.youtube.com/..." embeds are skipped;
update the selector used with parsed.$ (where getYouTubeID, replaceWith, and
embedCard.render are used) to also match "https://www.youtube.com" (and
generally any youtube.com embed form) — for example, change the selector to
include multiple attribute matches like 'iframe[src^="https://youtube"],
iframe[src^="https://www.youtube.com"], iframe[src*="youtube.com/embed"]' so
getYouTubeID sees those src values and the existing replaceWith(...) logic runs
for them too.
- Around line 114-117: The subscribe link rewrite currently sets anchor hrefs to
"/#/portal/signup", which forces navigation to the site root; update the
attr(...) call in the parsed.$('a[href*="/subscribe"]').forEach callback to set
the href to "#/portal/signup" instead so imported subscribe buttons open the
Portal signup in-place on the current post (locate the parsed.$ callback and the
attr(el, 'href', ...) invocation and change the value).
- Around line 526-530: The comparison of firstImageSplit[1] and
featureImageSplit[1] can yield false positives when either URL does not contain
"/uploads/asset/" (both split()[1] become undefined); update the logic around
firstImgSrc and featureSrc (the block where firstImageSplit and
featureImageSplit are created) to first verify that both strings actually
include "/uploads/asset/" (or that split produced length > 1) before comparing
the [1] segments, and only remove the first body image when both segments exist
and are equal.
In `@packages/mg-beehiiv-api/tsconfig.json`:
- Line 110: The tsconfig "include" array contains a stale entry "src/index.js"
which doesn't exist and is redundant given "src/**/*"; remove the "src/index.js"
string from the include array in the tsconfig so only "src/**/*" remains,
ensuring the project uses the actual TypeScript entry (src/index.ts) and avoids
misleading JavaScript references.
In `@packages/migrate/commands/beehiiv-api.js`:
- Around line 145-162: The publication discovery call
mgBeehiiv.listPublications() should be moved inside the try block so API
failures are caught and routed to ui.log.error; remove any access to
getPubs.errors (listPublications returns an array or throws) and handle an empty
array by printing the table and warning without reading errors; also remove the
unnecessary process.exit(0) after printing the table so control can return
normally. Ensure the logic around printing the table (mapping pub.name, pub.id,
new Date(pub.created * 1000), pub.stats?.active_subscriptions) and the
ui.log.warn message remains the same.
---
Duplicate comments:
In `@packages/mg-beehiiv-api/src/lib/mapper.ts`:
- Around line 11-13: The mapping currently sets updated_at from postData.created
which hides edits; in mapper.ts update the mapping for updated_at to use
Beehiiv's real update timestamp when present (e.g., check postData.updated,
postData.updated_at, or whatever the API field is) and convert it to a Date (new
Date(postData.<updateField> * 1000)); if no update timestamp exists, omit
updated_at (or set it to undefined) instead of copying created_at. Locate the
mapping that sets published_at, updated_at, created_at and replace the
updated_at assignment accordingly.
In `@packages/migrate/commands/beehiiv-api-members.js`:
- Around line 96-115: Move the "no --id" branch so that the call to
mgBeehiivApiMembers.listPublications(...) happens inside the existing try block
(so failures are caught by the catch), and replace process.exit(0) with a plain
return to allow the command to return normally; keep the same ui.log.error and
ui.log.warn messages but ensure any thrown errors from listPublications are
handled by the catch block rather than escaping it.
---
Nitpick comments:
In `@packages/mg-beehiiv-api/src/lib/fetch.ts`:
- Around line 68-79: The date filters (options.postsAfter and
options.postsBefore) are currently applied to ctx.result.posts inside the
pagination loop, causing repeated full-array scans; move the filtering logic out
of the loop so you fetch and append all pages first (the code that populates
ctx.result.posts), then after pagination completes apply a single filter pass
using options.postsAfter and options.postsBefore (convert to timestamps once,
e.g. new Date(...).getTime()/1000) to produce the final ctx.result.posts;
update/remove the in-loop blocks that reference ctx.result.posts.filter and
ensure you only compare post.publish_date to the computed
afterTimestamp/beforeTimestamp once after all pages are aggregated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0af73861-c6ec-48ae-bbd2-04c5aeddff12
⛔ Files ignored due to path filters (2)
packages/mg-beehiiv-api/src/test/fixtures/posts.csvis excluded by!**/*.csvyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (37)
packages/mg-beehiiv-api-members/.eslintrc.cjspackages/mg-beehiiv-api-members/README.mdpackages/mg-beehiiv-api-members/package.jsonpackages/mg-beehiiv-api-members/src/index.tspackages/mg-beehiiv-api-members/src/lib/fetch.tspackages/mg-beehiiv-api-members/src/lib/list-pubs.tspackages/mg-beehiiv-api-members/src/lib/mapper.tspackages/mg-beehiiv-api-members/src/test/fetch.test.tspackages/mg-beehiiv-api-members/src/test/index.test.tspackages/mg-beehiiv-api-members/src/test/mapper.test.tspackages/mg-beehiiv-api-members/src/types.d.tspackages/mg-beehiiv-api-members/tsconfig.jsonpackages/mg-beehiiv-api/.eslintrc.cjspackages/mg-beehiiv-api/.gitignorepackages/mg-beehiiv-api/LICENSEpackages/mg-beehiiv-api/README.mdpackages/mg-beehiiv-api/package.jsonpackages/mg-beehiiv-api/src/index.tspackages/mg-beehiiv-api/src/lib/client.tspackages/mg-beehiiv-api/src/lib/fetch.tspackages/mg-beehiiv-api/src/lib/list-pubs.tspackages/mg-beehiiv-api/src/lib/mapper.tspackages/mg-beehiiv-api/src/lib/process.tspackages/mg-beehiiv-api/src/test/client.test.tspackages/mg-beehiiv-api/src/test/fetch.test.tspackages/mg-beehiiv-api/src/test/index.test.tspackages/mg-beehiiv-api/src/test/list-pubs.test.tspackages/mg-beehiiv-api/src/test/mapper.test.tspackages/mg-beehiiv-api/src/test/process.test.tspackages/mg-beehiiv-api/src/types.d.tspackages/mg-beehiiv-api/tsconfig.jsonpackages/migrate/bin/cli.jspackages/migrate/commands/beehiiv-api-members.jspackages/migrate/commands/beehiiv-api.jspackages/migrate/package.jsonpackages/migrate/sources/beehiiv-api-members.jspackages/migrate/sources/beehiiv-api.js
🚧 Files skipped from review as they are similar to previous changes (19)
- packages/mg-beehiiv-api-members/README.md
- packages/migrate/package.json
- packages/mg-beehiiv-api-members/src/test/index.test.ts
- packages/mg-beehiiv-api-members/package.json
- packages/mg-beehiiv-api/src/lib/client.ts
- packages/migrate/sources/beehiiv-api.js
- packages/mg-beehiiv-api/src/test/list-pubs.test.ts
- packages/mg-beehiiv-api/.gitignore
- packages/mg-beehiiv-api/LICENSE
- packages/migrate/sources/beehiiv-api-members.js
- packages/mg-beehiiv-api-members/src/test/fetch.test.ts
- packages/mg-beehiiv-api/README.md
- packages/mg-beehiiv-api/src/test/fetch.test.ts
- packages/mg-beehiiv-api-members/tsconfig.json
- packages/mg-beehiiv-api-members/.eslintrc.cjs
- packages/mg-beehiiv-api/src/test/index.test.ts
- packages/mg-beehiiv-api/src/types.d.ts
- packages/mg-beehiiv-api-members/src/types.d.ts
- packages/mg-beehiiv-api/.eslintrc.cjs
| import {listPublications} from './lib/list-pubs.js'; | ||
| import {fetchTasks} from './lib/fetch.js'; | ||
| import {mapMembersTasks} from './lib/mapper.js'; | ||
|
|
||
| export default { | ||
| listPublications, | ||
| fetchTasks, | ||
| mapMembersTasks | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Export these utilities by name from the package entrypoint.
This barrel only exposes free functions, so the default object export is fighting the repo’s index.ts convention and forces namespace-style consumption everywhere.
♻️ Proposed change
-import {listPublications} from './lib/list-pubs.js';
-import {fetchTasks} from './lib/fetch.js';
-import {mapMembersTasks} from './lib/mapper.js';
-
-export default {
- listPublications,
- fetchTasks,
- mapMembersTasks
-};
+export {listPublications} from './lib/list-pubs.js';
+export {fetchTasks} from './lib/fetch.js';
+export {mapMembersTasks} from './lib/mapper.js';As per coding guidelines, "Named exports should be used for utilities, and default exports for main functionality (classes)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api-members/src/index.ts` around lines 1 - 9, Replace the
default object export with named exports for the utility functions; instead of
exporting a default object containing listPublications, fetchTasks, and
mapMembersTasks, export each symbol by name (listPublications, fetchTasks,
mapMembersTasks) so consumers can import them directly (e.g., import {
listPublications } from ...). Update the package entrypoint to use named exports
for the listed functions and remove the default export.
| if (postData.thumbnail_url) { | ||
| mappedData.data.feature_image = postData.thumbnail_url; | ||
| } | ||
|
|
||
| if (postData.meta_default_title) { | ||
| mappedData.data.og_title = postData.meta_default_title; | ||
| } | ||
|
|
||
| if (postData.meta_default_description) { | ||
| mappedData.data.og_description = postData.meta_default_description; | ||
| } | ||
|
|
||
| mappedData.data.html = processHTML({ | ||
| post: mappedData, | ||
| options | ||
| }); |
There was a problem hiding this comment.
Apply the duplicate-feature-image pass after processHTML().
feature_image is populated above and removeDuplicateFeatureImage is already imported, but the processed HTML is returned unchanged. Posts whose hero is repeated as the first body image will still import with the image twice.
🐛 Proposed fix
mappedData.data.html = processHTML({
post: mappedData,
options
});
+
+ if (mappedData.data.feature_image) {
+ mappedData.data.html = removeDuplicateFeatureImage({
+ html: mappedData.data.html,
+ featureSrc: mappedData.data.feature_image
+ });
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api/src/lib/mapper.ts` around lines 25 - 40, The code
sets mappedData.data.feature_image before calling processHTML, but never runs
the removeDuplicateFeatureImage pass afterward, so hero images duplicated as the
first body image remain; after you compute mappedData.data.html via
processHTML({ post: mappedData, options }) call
removeDuplicateFeatureImage(mappedData) (or
removeDuplicateFeatureImage(mappedData.data.html, mappedData.data.feature_image)
depending on that helper's signature) and assign its result back into
mappedData.data.html (or update mappedData accordingly) so the duplicate feature
image is stripped from the rendered HTML.
🤖 Velo CI Failure AnalysisClassification: 🟠 SOFT FAIL
|
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
packages/migrate/commands/beehiiv-api-members.js (1)
96-115:⚠️ Potential issue | 🟠 MajorKeep the publication-listing branch inside the existing
tryand return normally.
listPublications(argv.key)still runs before thecatch, so any rejection skipsui.log.error().process.exit(0)also hard-stops the CLI in a path that can justreturnafter printing the table and warning.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api-members.js` around lines 96 - 115, The publication-listing branch currently calls mgBeehiivApiMembers.listPublications(argv.key) outside the try/catch and calls process.exit(0); move the entire block (the listPublications call, the getPubs check, console.table, and ui.log.warn) inside the same try that wraps the migration logic so any rejection is caught by the existing catch, and replace process.exit(0) with a plain return so the CLI returns normally after printing the table and warning; ensure you keep the ui.log.error usage in the catch to log API rejections.packages/migrate/sources/beehiiv-api.js (1)
68-68:⚠️ Potential issue | 🟠 MajorNormalize every
ctx.errors.push(...)to one structured payload.These catches still push
(message, error)while Line 56 pushes an object, soctx.errorsends up heterogeneous. Any downstream consumer expecting a consistent{message, error}shape will mis-handle part of the pipeline's failures.Suggested fix
- ctx.errors.push('Failed to process beehiiv content', error); + ctx.errors.push({message: 'Failed to process beehiiv content', error}); - ctx.errors.push('Failed to build link map', error); + ctx.errors.push({message: 'Failed to build link map', error}); - ctx.errors.push('Failed to format data as Ghost JSON', error); + ctx.errors.push({message: 'Failed to format data as Ghost JSON', error}); - ctx.errors.push('Failed to convert HTML -> Lexical', error); + ctx.errors.push({message: 'Failed to convert HTML -> Lexical', error}); - ctx.errors.push('Failed to write Ghost import JSON File', error); + ctx.errors.push({message: 'Failed to write Ghost import JSON File', error}); - ctx.errors.push('Failed to write and upload ZIP file', error); + ctx.errors.push({message: 'Failed to write and upload ZIP file', error}); - ctx.errors.push('Failed to clear temporary cached files', error); + ctx.errors.push({message: 'Failed to clear temporary cached files', error});As per coding guidelines, "
**/*.{ts,js}: Use@tryghost/errorsfor structured error handling with message and context properties."Also applies to: 80-80, 92-92, 126-126, 138-138, 170-170, 182-182
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/sources/beehiiv-api.js` at line 68, The ctx.errors array is getting heterogeneous entries because some catches call ctx.errors.push('Failed to process beehiiv content', error) while others push an object; update every ctx.errors.push call in this file (e.g., the pushes that use the literal message 'Failed to process beehiiv content' and the occurrences at the mentioned locations) to push a single structured payload with consistent shape — use `@tryghost/errors` to create a structured error (include message and context/details) or wrap into an object like {message: 'Failed to process beehiiv content', error: /* original error */} so all entries written by ctx.errors.push share the same {message, error} (or {message, context}) structure.
🧹 Nitpick comments (10)
packages/mg-beehiiv-api-members/src/lib/fetch.ts (1)
28-34: Consider replacinganytypes with proper interfaces.Using
anyforfileCache,options, andctxloses type safety. Based on context snippet 3, these have known structures that could be typed.Also applies to: 63-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/src/lib/fetch.ts` around lines 28 - 34, Replace the loose any types by declaring and using explicit interfaces for the cache and surrounding call shapes: define a FileCache interface (methods/properties used by cachedFetch), an Options interface for the `options` object, and a Context (ctx) interface matching what's used elsewhere, then update the cachedFetch signature to use FileCache instead of any and replace other occurrences (e.g., the `options` and `ctx` parameters referenced around symbol names `options` and `ctx`) to use those interfaces; ensure exported/locally-scoped types match the fields observed in the surrounding code so TypeScript can enforce correct usage.packages/mg-beehiiv-api-members/src/lib/list-pubs.ts (1)
13-15: Consider adding return type annotation and null safety.The function returns
data.datawithout type validation. If the API response structure changes ordata.datais undefined, this could cause runtime issues downstream. The caller inbeehiiv-api-members.js(lines 101-109) accesses properties likepub.name,pub.id,pub.created, andpub.stats?.active_subscriptions.🛡️ Proposed type-safe return
- const data = await response.json(); - - return data.data; + const data = await response.json(); + + return data.data ?? [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/src/lib/list-pubs.ts` around lines 13 - 15, The code returns data.data without type safety; update the function in list-pubs.ts (e.g., listPubs) to declare an explicit return type (for example Promise<Pub[]>), define a Pub interface including id, name, created and optional stats.active_subscriptions, and validate the parsed JSON before returning: check that response.ok, that data is an object and Array.isArray(data.data), and either return data.data as Pub[] after a runtime guard or return an empty array / throw a descriptive error if the shape is invalid so callers (which access pub.name, pub.id, pub.created, pub.stats?.active_subscriptions) won't encounter undefined properties. Ensure you use type assertions only after the runtime checks.packages/mg-beehiiv-api-members/src/test/mapper.test.ts (2)
336-351: Make theError-path test deterministic.
packages/mg-beehiiv-api-members/src/lib/mapper.ts:89-95explicitly copieserror.messageintotask.output, but this test currently relies on an incidentalTypeErrorfrom invalid input and only checks for a non-empty string. That makes the contract easy to regress without failing the test.💡 Suggested change
it('task sets error output on failure with Error instance', async () => { - const ctx = { + const ctx: any = { result: { - subscriptions: 'not an array' // Invalid data + get subscriptions() { + throw new Error('Custom error'); + } } }; const tasks = mapMembersTasks({}, ctx); const mockTask = {output: ''}; @@ await assert.rejects(async () => { await tasks[0].task({}, mockTask); }); - assert.ok(mockTask.output.length > 0); + assert.equal(mockTask.output, 'Custom error'); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/src/test/mapper.test.ts` around lines 336 - 351, The test should deterministically verify that the task copies the thrown Error.message into task.output: call mapMembersTasks to get tasks, invoke tasks[0].task and capture the rejected Error (e.g. via try/catch or .catch(err => ...)), then assert that mockTask.output === err.message (instead of only checking mockTask.output.length > 0); reference mapMembersTasks and mockTask.output to locate the change and replace the assert.rejects + length check with an assertion that output equals the captured error.message.
353-371: Remove the synthetic raw string throw test case.This test disables
no-throw-literaland models an error shape the codebase explicitly discourages per coding guidelines ("Use@tryghost/errorsfor structured error handling with message and context properties"). No source code in the Beehiiv packages throws raw values—only test files do so with explicit linting disables. Keep the failure-path coverage onErroror@tryghost/errorsinstances instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api-members/src/test/mapper.test.ts` around lines 353 - 371, Remove the synthetic raw string throw test "task sets error output on failure with non-Error thrown" and replace it with a failure-path test that throws an actual Error (or an instance of `@tryghost/errors`) so it conforms to project error-handling guidelines; locate the test in mapper.test.ts that constructs ctx.result.get subscriptions throwing 'Custom string error' and either delete that block or change the thrown value to new Error('Custom string error') (or a `@tryghost/errors` instance) and assert mockTask.output is set accordingly when invoking mapMembersTasks and executing tasks[0].task.packages/mg-beehiiv-api/src/lib/fetch.ts (1)
68-79: Move date filtering outside the pagination loop.The date filters are applied to the entire
ctx.result.postsarray on every page iteration, which is inefficient. Posts from earlier pages are re-filtered unnecessarily.♻️ Proposed fix - filter once after all pages are fetched
Move filtering to after the loop completes, or apply it to
fetchTaskscaller:- if (options.postsAfter) { - const afterTimestamp = new Date(options.postsAfter).getTime() / 1000; - ctx.result.posts = ctx.result.posts.filter( - (post: any) => post.publish_date >= afterTimestamp - ); - } - if (options.postsBefore) { - const beforeTimestamp = new Date(options.postsBefore).getTime() / 1000; - ctx.result.posts = ctx.result.posts.filter( - (post: any) => post.publish_date <= beforeTimestamp - ); - } } catch (error) {Then add a final task or post-processing step to apply filtering once.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/fetch.ts` around lines 68 - 79, The date filtering for options.postsAfter/options.postsBefore is being applied to ctx.result.posts inside the pagination loop (causing repeated re-filtering); move the filter logic out of the loop and apply it once after all pages are fetched (e.g., after the fetch/pagination loop in fetchTasks or as a final post-processing step) so that ctx.result.posts is filtered a single time using new Date(options.postsAfter).getTime()/1000 and new Date(options.postsBefore).getTime()/1000 on the final aggregate array.packages/mg-beehiiv-api/src/lib/process.ts (1)
205-475: Remove or address the extensive commented-out code.There are ~270 lines of commented-out code. This creates maintenance burden and confusion. Consider:
- Removing it if no longer needed
- Moving to a separate branch/issue if it's WIP
- Adding a TODO comment explaining why it's kept if intentional
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/process.ts` around lines 205 - 475, The file contains ~270 lines of large commented-out transformation logic (many $html(...) blocks including image handling, embedCard.render, imageCard.render, bookmarkCard.render, YouTube handling and options?.url branches) which should be cleaned up; either delete these commented sections if obsolete, move them to a dedicated WIP branch or a new file (e.g., archive the legacy html-to-ghost conversion helpers), or replace the block with a short TODO comment explaining why the code is retained and linking to an issue/branch/PR; update references around the functions/constructs mentioned ($html, imageCard.render, embedCard.render, bookmarkCard.render, getYouTubeID, options?.url) so callers are clear and ensure any tests or docs note where the archived logic lives.packages/mg-beehiiv-api/src/lib/client.ts (1)
1-22: Consider consolidating withauthedClientinfetch.ts.This
clientfunction duplicates the authenticated fetch pattern already implemented byauthedClientinfetch.ts(see context snippet). The only difference is this hardcodes the publications URL. Consider either:
- Reusing
authedClienthere, or- Removing this file and using
listPublicationsfromlist-pubs.tswhich already does this♻️ Proposed consolidation
+import {authedClient} from './fetch.js'; + const client = async (apiKey: string) => { const url = new URL(`https://api.beehiiv.com/v2/publications`); - - const response = await fetch(url, { - method: 'GET', - headers: { - Authorization: `Bearer ${apiKey}` - } - }); + const response = await authedClient(apiKey, url); if (!response.ok) { throw new Error(`Request failed: ${response.status} ${response.statusText}`); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/client.ts` around lines 1 - 22, The client function in client.ts duplicates the authenticated fetch pattern implemented by authedClient (fetch.ts) and/or the listPublications helper (list-pubs.ts); refactor by either replacing client to call authedClient with the publications path and the provided apiKey (so client delegates URL construction and error handling to authedClient) or delete client.ts and update callers to use listPublications from list-pubs.ts; references to change: client (in client.ts), authedClient (in fetch.ts), and listPublications (in list-pubs.ts).packages/mg-beehiiv-api/src/test/fetch.test.ts (2)
23-27: Assert the auth header viaHeaders, not object shape.
RequestInit.headersis not guaranteed to be a plain object. This assertion will fail on a valid implementation that passes aHeadersinstance or tuple array.Suggested change
assert.equal(fetchMock.mock.callCount(), 1); const [calledUrl, options] = fetchMock.mock.calls[0].arguments; assert.equal(calledUrl.toString(), 'https://api.beehiiv.com/v2/publications'); assert.equal(options.method, 'GET'); - assert.equal(options.headers.Authorization, 'Bearer test-api-key'); + assert.equal(new Headers(options.headers).get('Authorization'), 'Bearer test-api-key');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/test/fetch.test.ts` around lines 23 - 27, The test currently asserts options.headers as a plain object which fails when implementations supply a Headers instance; update the assertion to normalize and read the Authorization header via the Headers API (e.g., construct a Headers from RequestInit.headers or call .get on it) using the existing variables fetchMock.mock.calls, calledUrl and options; ensure you assert that new Headers(options.headers).get('Authorization') equals 'Bearer test-api-key' and keep the URL and method assertions as-is.
91-116: Add one cache-path filter test.The suite verifies caching and date filtering separately, but not together. If the cached branch ever bypasses
postsAfter/postsBefore, these tests would still stay green.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/test/fetch.test.ts` around lines 91 - 116, The test is missing coverage that cached responses are still subject to the same date/path filters; update or add a test that calls fetchTasks with filtering options (e.g., postsAfter/postsBefore or the cache-path filter param used by fetchTasks) and a ctx.fileCache that returns cachedData via readTmpJSONFile, then run tasks[0].task and assert the cached posts are filtered (only expected items remain) and that fetchMock was only called once (discover); reference fetchTasks, tasks[0].task, ctx.fileCache.readTmpJSONFile, and the postsAfter/postsBefore (or cache-path) filter parameters when implementing the test.packages/migrate/sources/beehiiv-api.js (1)
1-1: Replace synchronous file I/O with async variant in upload path.
readFileSync()blocks the event loop, andawait readFileSync()does not perform asynchronous I/O—the file read completes synchronously beforeawaitwraps it in a Promise. For large export ZIPs, this will stall the task runner immediately before upload.Suggested fix
-import {readFileSync} from 'node:fs'; +import {readFile} from 'node:fs/promises'; ... - const fileBuffer = await readFileSync(ctx.outputFile.path); + const fileBuffer = await readFile(ctx.outputFile.path);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/sources/beehiiv-api.js` at line 1, The code currently imports and uses readFileSync which performs blocking I/O; replace readFileSync with an async file-read (e.g., use readFile from 'node:fs/promises' or import { readFile } from 'node:fs' and use the promises API) and await the promise in the upload path so the event loop is not blocked before the upload; update the import (remove readFileSync) and change the code that reads the export ZIP (the place using readFileSync) to call and await the async read function before passing the buffer to the upload routine.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/mg-beehiiv-api-members/src/lib/fetch.ts`:
- Around line 63-65: The code in fetchTasks calling discover(options.key,
options.id) can return undefined leading to NaN when computing estimatedPages;
update fetchTasks to coerce totalSubscriptions to a safe number (e.g., const
totalSubscriptions = Number(await discover(...)) || 0) and compute
estimatedPages defensively (e.g., const estimatedPages = totalSubscriptions > 0
? Math.ceil(totalSubscriptions / API_LIMIT) : 0) and ensure any progress/logging
paths handle the zero-pages case appropriately; reference fetchTasks, discover,
API_LIMIT and estimatedPages when making these changes.
- Around line 1-10: The file references the types BeehiivPublicationResponse and
BeehiivSubscriptionsResponse but doesn't import them; add explicit type imports
for BeehiivPublicationResponse and BeehiivSubscriptionsResponse at the top of
this module so TypeScript can resolve the annotations used later in this file
(where those types are applied to fetch responses/subscription handling). Ensure
the import names match the exported type names from their source module and
place the imports alongside other top-level imports so functions like
authedClient and the subscription/publication response handlers compile cleanly.
In `@packages/mg-beehiiv-api/src/lib/process.ts`:
- Around line 119-124: The code in the link-embed handling block (inside the
parsed.$('.generic-embed--root').forEach callback) performs unsafe indexed
accesses like parsed.$('a', el)[0], parsed.$('.generic-embed--title p', el)[0],
parsed.$('.generic-embed--description p', el)[0], and
parsed.$('.generic-embed--image img', el)[0] before calling attr(...) or
.textContent, which can throw if nodes are missing; update the callback to
null-check each query result (or use optional chaining) before indexing and
reading properties, and provide safe fallbacks for href, title, description, and
image (e.g., undefined or empty string) so attr(parsed.$(...)[0] as Element,
...) and _.unescape(...textContent!) are only invoked when the element exists.
In `@packages/mg-beehiiv-api/src/test/fetch.test.ts`:
- Around line 158-162: The test currently simulates a fetch failure by
synchronously throwing a string inside fetchMock.mock.mockImplementationOnce;
update the mock to return a rejected promise instead (e.g. return
Promise.reject('Network error string')) so the async error path is exercised
like real fetch, keeping the same call to fetchMock.mock.mockImplementationOnce
in the test body that covers the failing response.
In `@packages/mg-beehiiv-api/src/types.d.ts`:
- Around line 16-17: The current type declarations in types.d.ts use literal
values for the fields displayed_date and split_tested which narrows their types;
update the type annotations (e.g., change displayed_date from the literal null
to an optional/nullable type like string | null or Date | null as appropriate,
and change split_tested from the literal false to boolean) so the interface/type
(where displayed_date and split_tested are declared) accurately reflects
possible runtime values.
- Around line 26-27: The properties meta_default_description and
meta_default_title are currently set to literal nulls; change their type
annotations so they accept either a string or null (e.g., string | null) instead
of a literal null. Locate these properties in the type or interface declaration
in packages/mg-beehiiv-api/src/types.d.ts (look for meta_default_description and
meta_default_title), update their types to string | null, and ensure any places
that construct or consume this type handle both string and null accordingly.
---
Duplicate comments:
In `@packages/migrate/commands/beehiiv-api-members.js`:
- Around line 96-115: The publication-listing branch currently calls
mgBeehiivApiMembers.listPublications(argv.key) outside the try/catch and calls
process.exit(0); move the entire block (the listPublications call, the getPubs
check, console.table, and ui.log.warn) inside the same try that wraps the
migration logic so any rejection is caught by the existing catch, and replace
process.exit(0) with a plain return so the CLI returns normally after printing
the table and warning; ensure you keep the ui.log.error usage in the catch to
log API rejections.
In `@packages/migrate/sources/beehiiv-api.js`:
- Line 68: The ctx.errors array is getting heterogeneous entries because some
catches call ctx.errors.push('Failed to process beehiiv content', error) while
others push an object; update every ctx.errors.push call in this file (e.g., the
pushes that use the literal message 'Failed to process beehiiv content' and the
occurrences at the mentioned locations) to push a single structured payload with
consistent shape — use `@tryghost/errors` to create a structured error (include
message and context/details) or wrap into an object like {message: 'Failed to
process beehiiv content', error: /* original error */} so all entries written by
ctx.errors.push share the same {message, error} (or {message, context})
structure.
---
Nitpick comments:
In `@packages/mg-beehiiv-api-members/src/lib/fetch.ts`:
- Around line 28-34: Replace the loose any types by declaring and using explicit
interfaces for the cache and surrounding call shapes: define a FileCache
interface (methods/properties used by cachedFetch), an Options interface for the
`options` object, and a Context (ctx) interface matching what's used elsewhere,
then update the cachedFetch signature to use FileCache instead of any and
replace other occurrences (e.g., the `options` and `ctx` parameters referenced
around symbol names `options` and `ctx`) to use those interfaces; ensure
exported/locally-scoped types match the fields observed in the surrounding code
so TypeScript can enforce correct usage.
In `@packages/mg-beehiiv-api-members/src/lib/list-pubs.ts`:
- Around line 13-15: The code returns data.data without type safety; update the
function in list-pubs.ts (e.g., listPubs) to declare an explicit return type
(for example Promise<Pub[]>), define a Pub interface including id, name, created
and optional stats.active_subscriptions, and validate the parsed JSON before
returning: check that response.ok, that data is an object and
Array.isArray(data.data), and either return data.data as Pub[] after a runtime
guard or return an empty array / throw a descriptive error if the shape is
invalid so callers (which access pub.name, pub.id, pub.created,
pub.stats?.active_subscriptions) won't encounter undefined properties. Ensure
you use type assertions only after the runtime checks.
In `@packages/mg-beehiiv-api-members/src/test/mapper.test.ts`:
- Around line 336-351: The test should deterministically verify that the task
copies the thrown Error.message into task.output: call mapMembersTasks to get
tasks, invoke tasks[0].task and capture the rejected Error (e.g. via try/catch
or .catch(err => ...)), then assert that mockTask.output === err.message
(instead of only checking mockTask.output.length > 0); reference mapMembersTasks
and mockTask.output to locate the change and replace the assert.rejects + length
check with an assertion that output equals the captured error.message.
- Around line 353-371: Remove the synthetic raw string throw test "task sets
error output on failure with non-Error thrown" and replace it with a
failure-path test that throws an actual Error (or an instance of
`@tryghost/errors`) so it conforms to project error-handling guidelines; locate
the test in mapper.test.ts that constructs ctx.result.get subscriptions throwing
'Custom string error' and either delete that block or change the thrown value to
new Error('Custom string error') (or a `@tryghost/errors` instance) and assert
mockTask.output is set accordingly when invoking mapMembersTasks and executing
tasks[0].task.
In `@packages/mg-beehiiv-api/src/lib/client.ts`:
- Around line 1-22: The client function in client.ts duplicates the
authenticated fetch pattern implemented by authedClient (fetch.ts) and/or the
listPublications helper (list-pubs.ts); refactor by either replacing client to
call authedClient with the publications path and the provided apiKey (so client
delegates URL construction and error handling to authedClient) or delete
client.ts and update callers to use listPublications from list-pubs.ts;
references to change: client (in client.ts), authedClient (in fetch.ts), and
listPublications (in list-pubs.ts).
In `@packages/mg-beehiiv-api/src/lib/fetch.ts`:
- Around line 68-79: The date filtering for
options.postsAfter/options.postsBefore is being applied to ctx.result.posts
inside the pagination loop (causing repeated re-filtering); move the filter
logic out of the loop and apply it once after all pages are fetched (e.g., after
the fetch/pagination loop in fetchTasks or as a final post-processing step) so
that ctx.result.posts is filtered a single time using new
Date(options.postsAfter).getTime()/1000 and new
Date(options.postsBefore).getTime()/1000 on the final aggregate array.
In `@packages/mg-beehiiv-api/src/lib/process.ts`:
- Around line 205-475: The file contains ~270 lines of large commented-out
transformation logic (many $html(...) blocks including image handling,
embedCard.render, imageCard.render, bookmarkCard.render, YouTube handling and
options?.url branches) which should be cleaned up; either delete these commented
sections if obsolete, move them to a dedicated WIP branch or a new file (e.g.,
archive the legacy html-to-ghost conversion helpers), or replace the block with
a short TODO comment explaining why the code is retained and linking to an
issue/branch/PR; update references around the functions/constructs mentioned
($html, imageCard.render, embedCard.render, bookmarkCard.render, getYouTubeID,
options?.url) so callers are clear and ensure any tests or docs note where the
archived logic lives.
In `@packages/mg-beehiiv-api/src/test/fetch.test.ts`:
- Around line 23-27: The test currently asserts options.headers as a plain
object which fails when implementations supply a Headers instance; update the
assertion to normalize and read the Authorization header via the Headers API
(e.g., construct a Headers from RequestInit.headers or call .get on it) using
the existing variables fetchMock.mock.calls, calledUrl and options; ensure you
assert that new Headers(options.headers).get('Authorization') equals 'Bearer
test-api-key' and keep the URL and method assertions as-is.
- Around line 91-116: The test is missing coverage that cached responses are
still subject to the same date/path filters; update or add a test that calls
fetchTasks with filtering options (e.g., postsAfter/postsBefore or the
cache-path filter param used by fetchTasks) and a ctx.fileCache that returns
cachedData via readTmpJSONFile, then run tasks[0].task and assert the cached
posts are filtered (only expected items remain) and that fetchMock was only
called once (discover); reference fetchTasks, tasks[0].task,
ctx.fileCache.readTmpJSONFile, and the postsAfter/postsBefore (or cache-path)
filter parameters when implementing the test.
In `@packages/migrate/sources/beehiiv-api.js`:
- Line 1: The code currently imports and uses readFileSync which performs
blocking I/O; replace readFileSync with an async file-read (e.g., use readFile
from 'node:fs/promises' or import { readFile } from 'node:fs' and use the
promises API) and await the promise in the upload path so the event loop is not
blocked before the upload; update the import (remove readFileSync) and change
the code that reads the export ZIP (the place using readFileSync) to call and
await the async read function before passing the buffer to the upload routine.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4391a86d-2db0-477a-865c-bd736133b71c
⛔ Files ignored due to path filters (2)
packages/mg-beehiiv-api/src/test/fixtures/posts.csvis excluded by!**/*.csvyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (37)
packages/mg-beehiiv-api-members/.eslintrc.cjspackages/mg-beehiiv-api-members/README.mdpackages/mg-beehiiv-api-members/package.jsonpackages/mg-beehiiv-api-members/src/index.tspackages/mg-beehiiv-api-members/src/lib/fetch.tspackages/mg-beehiiv-api-members/src/lib/list-pubs.tspackages/mg-beehiiv-api-members/src/lib/mapper.tspackages/mg-beehiiv-api-members/src/test/fetch.test.tspackages/mg-beehiiv-api-members/src/test/index.test.tspackages/mg-beehiiv-api-members/src/test/mapper.test.tspackages/mg-beehiiv-api-members/src/types.d.tspackages/mg-beehiiv-api-members/tsconfig.jsonpackages/mg-beehiiv-api/.eslintrc.cjspackages/mg-beehiiv-api/.gitignorepackages/mg-beehiiv-api/LICENSEpackages/mg-beehiiv-api/README.mdpackages/mg-beehiiv-api/package.jsonpackages/mg-beehiiv-api/src/index.tspackages/mg-beehiiv-api/src/lib/client.tspackages/mg-beehiiv-api/src/lib/fetch.tspackages/mg-beehiiv-api/src/lib/list-pubs.tspackages/mg-beehiiv-api/src/lib/mapper.tspackages/mg-beehiiv-api/src/lib/process.tspackages/mg-beehiiv-api/src/test/client.test.tspackages/mg-beehiiv-api/src/test/fetch.test.tspackages/mg-beehiiv-api/src/test/index.test.tspackages/mg-beehiiv-api/src/test/list-pubs.test.tspackages/mg-beehiiv-api/src/test/mapper.test.tspackages/mg-beehiiv-api/src/test/process.test.tspackages/mg-beehiiv-api/src/types.d.tspackages/mg-beehiiv-api/tsconfig.jsonpackages/migrate/bin/cli.jspackages/migrate/commands/beehiiv-api-members.jspackages/migrate/commands/beehiiv-api.jspackages/migrate/package.jsonpackages/migrate/sources/beehiiv-api-members.jspackages/migrate/sources/beehiiv-api.js
✅ Files skipped from review due to trivial changes (1)
- packages/mg-beehiiv-api-members/src/test/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (19)
- packages/mg-beehiiv-api/src/test/client.test.ts
- packages/mg-beehiiv-api-members/README.md
- packages/mg-beehiiv-api-members/src/test/fetch.test.ts
- packages/mg-beehiiv-api-members/.eslintrc.cjs
- packages/migrate/sources/beehiiv-api-members.js
- packages/mg-beehiiv-api/tsconfig.json
- packages/mg-beehiiv-api/src/test/process.test.ts
- packages/mg-beehiiv-api-members/package.json
- packages/mg-beehiiv-api-members/src/lib/mapper.ts
- packages/mg-beehiiv-api-members/src/index.ts
- packages/migrate/bin/cli.js
- packages/mg-beehiiv-api/.eslintrc.cjs
- packages/migrate/commands/beehiiv-api.js
- packages/mg-beehiiv-api/src/test/list-pubs.test.ts
- packages/mg-beehiiv-api/LICENSE
- packages/mg-beehiiv-api/README.md
- packages/mg-beehiiv-api-members/src/types.d.ts
- packages/mg-beehiiv-api-members/tsconfig.json
- packages/mg-beehiiv-api/src/lib/mapper.ts
| export const fetchTasks = async (options: any, ctx: any) => { | ||
| const totalSubscriptions = await discover(options.key, options.id); | ||
| const estimatedPages = Math.ceil(totalSubscriptions / API_LIMIT); |
There was a problem hiding this comment.
Potential division issue if totalSubscriptions is undefined or zero.
If discover() returns undefined (when stats?.active_subscriptions is not set), line 65 will compute Math.ceil(undefined / 100) resulting in NaN. Additionally, if totalSubscriptions is 0, the estimated pages will be 0 which is technically correct but the progress message may be misleading.
🛡️ Proposed defensive handling
export const fetchTasks = async (options: any, ctx: any) => {
const totalSubscriptions = await discover(options.key, options.id);
- const estimatedPages = Math.ceil(totalSubscriptions / API_LIMIT);
+ const estimatedPages = totalSubscriptions ? Math.ceil(totalSubscriptions / API_LIMIT) : 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api-members/src/lib/fetch.ts` around lines 63 - 65, The
code in fetchTasks calling discover(options.key, options.id) can return
undefined leading to NaN when computing estimatedPages; update fetchTasks to
coerce totalSubscriptions to a safe number (e.g., const totalSubscriptions =
Number(await discover(...)) || 0) and compute estimatedPages defensively (e.g.,
const estimatedPages = totalSubscriptions > 0 ? Math.ceil(totalSubscriptions /
API_LIMIT) : 0) and ensure any progress/logging paths handle the zero-pages case
appropriately; reference fetchTasks, discover, API_LIMIT and estimatedPages when
making these changes.
| // Mock fetch that throws a non-Error value (string) | ||
| fetchMock.mock.mockImplementationOnce(() => { | ||
| // eslint-disable-next-line no-throw-literal | ||
| throw 'Network error string'; | ||
| }, 1); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "fetch.test.ts" -type fRepository: TryGhost/migrate
Length of output: 165
🏁 Script executed:
cat -n ./packages/mg-beehiiv-api/src/test/fetch.test.ts | head -180 | tail -50Repository: TryGhost/migrate
Length of output: 2152
🏁 Script executed:
cat -n ./packages/mg-beehiiv-api/src/test/fetch.test.ts | head -185 | tail -15Repository: TryGhost/migrate
Length of output: 652
🏁 Script executed:
find ./packages/mg-beehiiv-api -name "*.ts" -not -path "*/test/*" -not -path "*/node_modules/*" | grep -E "(fetch|index)" | head -10Repository: TryGhost/migrate
Length of output: 141
🏁 Script executed:
cat -n ./packages/mg-beehiiv-api/src/lib/fetch.tsRepository: TryGhost/migrate
Length of output: 3943
Use Promise rejection to match real fetch behavior.
fetch() failures surface via promise rejection, not synchronous throws. Replace the mock's synchronous throw with Promise.reject() to accurately test the async failure path.
Suggested change
- fetchMock.mock.mockImplementationOnce(() => {
- // eslint-disable-next-line no-throw-literal
- throw 'Network error string';
- }, 1);
+ fetchMock.mock.mockImplementationOnce(() => Promise.reject('Network error string'), 1);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Mock fetch that throws a non-Error value (string) | |
| fetchMock.mock.mockImplementationOnce(() => { | |
| // eslint-disable-next-line no-throw-literal | |
| throw 'Network error string'; | |
| }, 1); | |
| // Mock fetch that throws a non-Error value (string) | |
| fetchMock.mock.mockImplementationOnce(() => Promise.reject('Network error string'), 1); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api/src/test/fetch.test.ts` around lines 158 - 162, The
test currently simulates a fetch failure by synchronously throwing a string
inside fetchMock.mock.mockImplementationOnce; update the mock to return a
rejected promise instead (e.g. return Promise.reject('Network error string')) so
the async error path is exercised like real fetch, keeping the same call to
fetchMock.mock.mockImplementationOnce in the test body that covers the failing
response.
226f872 to
e67d2dc
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (6)
packages/mg-beehiiv-api/src/lib/client.ts (1)
13-15:⚠️ Potential issue | 🟠 MajorAdd request metadata to the thrown Ghost error.
This still throws a structured error without any request context, which makes API failures much harder to diagnose downstream. Include at least
status,statusText, andurlincontext.🧩 Suggested change
if (!response.ok) { - throw new errors.InternalServerError({message: `Request failed: ${response.status} ${response.statusText}`}); + throw new errors.InternalServerError({ + message: `Request failed: ${response.status} ${response.statusText}`, + context: { + status: response.status, + statusText: response.statusText, + url: url.toString() + } + }); }As per coding guidelines, "Use
@tryghost/errorsfor structured error handling with message and context properties."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/client.ts` around lines 13 - 15, The thrown structured error in the response handling (errors.InternalServerError) lacks request metadata; update the throw inside the response.ok check in client.ts to pass a context object containing at least response.status, response.statusText, and response.url (e.g., include properties status, statusText, url on the error's context) so downstream handlers get request details; keep using errors.InternalServerError but supply the additional context argument alongside the message.packages/migrate/commands/beehiiv-api-members.js (1)
101-115:⚠️ Potential issue | 🟠 MajorReturn from
run()instead of terminating the process.After printing the publication table you can just
return.listPublications()either returns an array or throws, sogetPubs?.errorsis dead, andprocess.exit(0)makes this command harder to reuse and test.🧩 Suggested change
if (!getPubs || !getPubs.length) { - ui.log.error('Error fetching publications:', getPubs?.errors || 'No publications found'); + ui.log.error('No publications found'); return; } @@ ui.log.warn('No publication ID provided. Please provide an ID using the --id flag to run the migration.'); - - process.exit(0); + return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api-members.js` around lines 101 - 115, The code in run() treats getPubs as possibly containing an errors property and calls process.exit(0) after logging the publications; instead, remove the dead getPubs?.errors check (listPublications() either returns an array or throws), change the error handling to rely on the thrown error or an empty-array check, and replace process.exit(0) with a simple return so listPublications() and the publication listing in run() (and the ui.log.error/ui.log.warn calls) are testable and the command is reusable.packages/mg-beehiiv-api/src/test/fetch.test.ts (1)
158-162:⚠️ Potential issue | 🟡 MinorMock fetch failures as rejected promises.
Real
fetch()failures reject the promise instead of throwing synchronously, so this case is not exercising the same async path as production code.🧩 Suggested change
- fetchMock.mock.mockImplementationOnce(() => { - // eslint-disable-next-line no-throw-literal - throw 'Network error string'; - }, 1); + fetchMock.mock.mockImplementationOnce(() => Promise.reject('Network error string'), 1);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/test/fetch.test.ts` around lines 158 - 162, The test currently simulates a fetch failure by throwing a string in fetchMock.mock.mockImplementationOnce, which doesn't match real fetch behavior; change that mock to return a rejected promise instead (e.g., have fetchMock.mock.mockImplementationOnce(() => Promise.reject('Network error string'), 1)) so the async rejection path exercised in the production code is used; update any assertions if they expect an Error object to instead handle the rejected value or wrap it in an Error as appropriate.packages/mg-beehiiv-api/src/lib/fetch.ts (1)
20-21:⚠️ Potential issue | 🟠 MajorPreserve request identifiers in the HTTP failures.
Both error paths drop metadata that is already in scope, so once these bubble through the task runner it's hard to tell which publication or page failed. Put the request details into
contextat each throw site.As per coding guidelines, "Use
@tryghost/errorsfor structured error handling with message and context properties."Also applies to: 44-45
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/lib/fetch.ts` around lines 20 - 21, The throw sites in this module (the InternalServerError thrown when response.ok is false and the similar throw around lines 44-45) drop useful metadata; update both error constructions to pass a context object with the request identifiers already in scope (e.g., requestId, publicationId, pageId, url, method or whatever request metadata variables exist) so the error uses errors.InternalServerError({message: "...", context: { ... }}). Locate the two throw sites (the current throw that uses response.status/response.statusText and the other throw at 44-45) and add the appropriate context property populated from the local request/metadata variables in this file so the task runner can trace which publication/page failed.packages/migrate/commands/beehiiv-api.js (1)
149-163:⚠️ Potential issue | 🟠 MajorHandle the empty discovery path without touching
.errorsor exiting the process.
listPublications()returns an array or throws, sogetPubs.errorsis not a valid fallback here. In the falsy case it can also throw a secondaryTypeError, andprocess.exit(0)is unnecessary once the table has been shown.🧩 Suggested change
if (!getPubs || !getPubs.length) { - ui.log.error('Error fetching publications:', getPubs.errors); + ui.log.error('No publications found'); return; } @@ ui.log.warn('No publication ID provided. Please provide an ID using the --id flag to run the migration.'); - - process.exit(0); + return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/migrate/commands/beehiiv-api.js` around lines 149 - 163, The check for publications uses getPubs.errors and forcibly exits the process; fix by treating listPublications() as returning an array (or throwing) — remove any access to getPubs.errors and do not call process.exit(0). Specifically, in the code around listPublications/getPubs and the UI logging, replace the falsy branch to simply log a warning via ui.log.warn or ui.log.info when getPubs is empty (e.g., if (!getPubs || getPubs.length === 0) { ui.log.warn('No publications found'); return; }) and ensure any thrown errors from listPublications() are handled by a surrounding try/catch that logs the caught error via ui.log.error. Also remove the unconditional process.exit(0) after the console.table so the caller can continue.packages/mg-beehiiv-api/src/types.d.ts (1)
9-77:⚠️ Potential issue | 🟠 MajorMake these declaration aliases module-scoped.
beehiivPostDataObject,tagsObject,authorsObject, andmappedDataObjectare ambient globals right now, so sibling modules can consume them withoutimport type. That creates collision risk and makes the type surface brittle; export the aliases (or move the baredeclare module ...shims into a separate.d.ts) and import them explicitly where they’re used.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/mg-beehiiv-api/src/types.d.ts` around lines 9 - 77, The types beehiivPostDataObject, tagsObject, authorsObject, and mappedDataObject are currently declared as ambient globals; change them to module-scoped exports so other modules must import them. Update the declarations by exporting each type (e.g., export type beehiivPostDataObject = { ... }) or wrap them in a module and export, and then replace any usage sites to import type { beehiivPostDataObject, tagsObject, authorsObject, mappedDataObject } from the file where you move them; alternatively move these bare shims into a dedicated .d.ts that explicitly exports them to avoid global collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/mg-beehiiv-api-members/src/lib/fetch.ts`:
- Around line 37-40: The tmp cache filename is not namespaced by pubId so
different publications collide; update the filename construction in this
function to include pubId (e.g., change the filename variable used with
fileCache.hasFile and fileCache.readTmpJSONFile from
`beehiiv_api_members_${cursorIndex}.json` to include pubId, such as
`${pubId}_beehiiv_api_members_${cursorIndex}.json`) so all fileCache operations
(hasFile/readTmpJSONFile and any corresponding writes) use the pubId-prefixed
key.
In `@packages/mg-beehiiv-api-members/src/lib/list-pubs.ts`:
- Around line 10-11: The throw currently only sets a message; change it to pass
a structured context to errors.InternalServerError that includes the failing
endpoint metadata (e.g. response.status and response.statusText) and a
non-sensitive form of the request URL (use the response.url but strip
query/credentials—e.g. new URL(response.url).pathname or origin+pathname) so the
context helps debugging without leaking API keys; update the throw that uses
errors.InternalServerError to include both message and a context object
containing { status: response.status, statusText: response.statusText, path:
<stripped-url-path> } referencing the existing response and
errors.InternalServerError symbols.
In `@packages/mg-beehiiv-api/src/lib/fetch.ts`:
- Around line 29-30: The cache filename in cachedFetch currently uses only limit
and page and can collide across publications; update the filename generation in
cachedFetch (function cachedFetch) to incorporate pubId (e.g., include pubId in
the template for filename or key) so cached files are namespaced by publication;
ensure any pubId values are safely encoded/sanitized when constructing the
filename so fileCache lookups and writes use the new pub-scoped key and avoid
cross-publication cache replay.
In `@packages/mg-beehiiv-api/src/lib/list-pubs.ts`:
- Around line 10-11: The thrown errors.InternalServerError lacks a structured
context field; update the error creation in list-pubs.ts where response is
checked (the response.ok block) to include a context object with the request
endpoint and HTTP status (use response.url and response.status) alongside the
message so the thrown new errors.InternalServerError contains both message and
context for diagnostics.
In `@packages/migrate/sources/beehiiv-api.js`:
- Around line 14-18: The workspace task's async handler (task: async (ctx, task)
=> { ... }) assumes ctx.errors and ctx.warnings exist; initialize ctx.errors =
ctx.errors || [] and ctx.warnings = ctx.warnings || [] at the start of that
function (and likewise in the analogous block around lines 55-57) so direct
callers using getTaskRunner() won't hit TypeError when pushing into those
arrays; update the task function(s) in packages/migrate/sources/beehiiv-api.js
to defensively create these arrays if missing.
- Around line 55-57: Add the `@tryghost/errors` dependency to
packages/migrate/package.json and import it into
packages/migrate/sources/beehiiv-api.js (e.g., const errors =
require('@tryghost/errors')); then replace all places that push plain objects
into ctx.errors (uses of ctx.errors.push({...})) with wrapped
repository-standard errors: ctx.errors.push(new
errors.InternalServerError({message: 'Failed to fetch content from beehiiv API',
error})); apply the same wrapping for each occurrence referenced (the other
messages) so every ctx.errors entry uses new
errors.InternalServerError({message: '...', error}) instead of a plain object.
---
Duplicate comments:
In `@packages/mg-beehiiv-api/src/lib/client.ts`:
- Around line 13-15: The thrown structured error in the response handling
(errors.InternalServerError) lacks request metadata; update the throw inside the
response.ok check in client.ts to pass a context object containing at least
response.status, response.statusText, and response.url (e.g., include properties
status, statusText, url on the error's context) so downstream handlers get
request details; keep using errors.InternalServerError but supply the additional
context argument alongside the message.
In `@packages/mg-beehiiv-api/src/lib/fetch.ts`:
- Around line 20-21: The throw sites in this module (the InternalServerError
thrown when response.ok is false and the similar throw around lines 44-45) drop
useful metadata; update both error constructions to pass a context object with
the request identifiers already in scope (e.g., requestId, publicationId,
pageId, url, method or whatever request metadata variables exist) so the error
uses errors.InternalServerError({message: "...", context: { ... }}). Locate the
two throw sites (the current throw that uses response.status/response.statusText
and the other throw at 44-45) and add the appropriate context property populated
from the local request/metadata variables in this file so the task runner can
trace which publication/page failed.
In `@packages/mg-beehiiv-api/src/test/fetch.test.ts`:
- Around line 158-162: The test currently simulates a fetch failure by throwing
a string in fetchMock.mock.mockImplementationOnce, which doesn't match real
fetch behavior; change that mock to return a rejected promise instead (e.g.,
have fetchMock.mock.mockImplementationOnce(() => Promise.reject('Network error
string'), 1)) so the async rejection path exercised in the production code is
used; update any assertions if they expect an Error object to instead handle the
rejected value or wrap it in an Error as appropriate.
In `@packages/mg-beehiiv-api/src/types.d.ts`:
- Around line 9-77: The types beehiivPostDataObject, tagsObject, authorsObject,
and mappedDataObject are currently declared as ambient globals; change them to
module-scoped exports so other modules must import them. Update the declarations
by exporting each type (e.g., export type beehiivPostDataObject = { ... }) or
wrap them in a module and export, and then replace any usage sites to import
type { beehiivPostDataObject, tagsObject, authorsObject, mappedDataObject } from
the file where you move them; alternatively move these bare shims into a
dedicated .d.ts that explicitly exports them to avoid global collisions.
In `@packages/migrate/commands/beehiiv-api-members.js`:
- Around line 101-115: The code in run() treats getPubs as possibly containing
an errors property and calls process.exit(0) after logging the publications;
instead, remove the dead getPubs?.errors check (listPublications() either
returns an array or throws), change the error handling to rely on the thrown
error or an empty-array check, and replace process.exit(0) with a simple return
so listPublications() and the publication listing in run() (and the
ui.log.error/ui.log.warn calls) are testable and the command is reusable.
In `@packages/migrate/commands/beehiiv-api.js`:
- Around line 149-163: The check for publications uses getPubs.errors and
forcibly exits the process; fix by treating listPublications() as returning an
array (or throwing) — remove any access to getPubs.errors and do not call
process.exit(0). Specifically, in the code around listPublications/getPubs and
the UI logging, replace the falsy branch to simply log a warning via ui.log.warn
or ui.log.info when getPubs is empty (e.g., if (!getPubs || getPubs.length ===
0) { ui.log.warn('No publications found'); return; }) and ensure any thrown
errors from listPublications() are handled by a surrounding try/catch that logs
the caught error via ui.log.error. Also remove the unconditional process.exit(0)
after the console.table so the caller can continue.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2ff9e38f-4e4d-48d0-b484-12d08061be05
⛔ Files ignored due to path filters (1)
packages/mg-beehiiv-api/src/test/fixtures/posts.csvis excluded by!**/*.csv
📒 Files selected for processing (37)
packages/mg-beehiiv-api-members/.eslintrc.cjspackages/mg-beehiiv-api-members/README.mdpackages/mg-beehiiv-api-members/package.jsonpackages/mg-beehiiv-api-members/src/index.tspackages/mg-beehiiv-api-members/src/lib/fetch.tspackages/mg-beehiiv-api-members/src/lib/list-pubs.tspackages/mg-beehiiv-api-members/src/lib/mapper.tspackages/mg-beehiiv-api-members/src/test/fetch.test.tspackages/mg-beehiiv-api-members/src/test/index.test.tspackages/mg-beehiiv-api-members/src/test/mapper.test.tspackages/mg-beehiiv-api-members/src/types.d.tspackages/mg-beehiiv-api-members/tsconfig.jsonpackages/mg-beehiiv-api/.eslintrc.cjspackages/mg-beehiiv-api/.gitignorepackages/mg-beehiiv-api/LICENSEpackages/mg-beehiiv-api/README.mdpackages/mg-beehiiv-api/package.jsonpackages/mg-beehiiv-api/src/index.tspackages/mg-beehiiv-api/src/lib/client.tspackages/mg-beehiiv-api/src/lib/fetch.tspackages/mg-beehiiv-api/src/lib/list-pubs.tspackages/mg-beehiiv-api/src/lib/mapper.tspackages/mg-beehiiv-api/src/lib/process.tspackages/mg-beehiiv-api/src/test/client.test.tspackages/mg-beehiiv-api/src/test/fetch.test.tspackages/mg-beehiiv-api/src/test/index.test.tspackages/mg-beehiiv-api/src/test/list-pubs.test.tspackages/mg-beehiiv-api/src/test/mapper.test.tspackages/mg-beehiiv-api/src/test/process.test.tspackages/mg-beehiiv-api/src/types.d.tspackages/mg-beehiiv-api/tsconfig.jsonpackages/migrate/bin/cli.jspackages/migrate/commands/beehiiv-api-members.jspackages/migrate/commands/beehiiv-api.jspackages/migrate/package.jsonpackages/migrate/sources/beehiiv-api-members.jspackages/migrate/sources/beehiiv-api.js
✅ Files skipped from review due to trivial changes (1)
- packages/mg-beehiiv-api/LICENSE
🚧 Files skipped from review as they are similar to previous changes (21)
- packages/mg-beehiiv-api-members/src/test/mapper.test.ts
- packages/mg-beehiiv-api-members/src/lib/mapper.ts
- packages/mg-beehiiv-api/src/lib/mapper.ts
- packages/mg-beehiiv-api/src/index.ts
- packages/mg-beehiiv-api/src/lib/process.ts
- packages/mg-beehiiv-api-members/src/index.ts
- packages/mg-beehiiv-api-members/tsconfig.json
- packages/mg-beehiiv-api/package.json
- packages/mg-beehiiv-api/src/test/list-pubs.test.ts
- packages/mg-beehiiv-api-members/README.md
- packages/mg-beehiiv-api/tsconfig.json
- packages/mg-beehiiv-api-members/src/test/index.test.ts
- packages/mg-beehiiv-api/src/test/process.test.ts
- packages/mg-beehiiv-api/src/test/mapper.test.ts
- packages/migrate/bin/cli.js
- packages/migrate/sources/beehiiv-api-members.js
- packages/migrate/package.json
- packages/mg-beehiiv-api/.eslintrc.cjs
- packages/mg-beehiiv-api-members/src/test/fetch.test.ts
- packages/mg-beehiiv-api/src/test/client.test.ts
- packages/mg-beehiiv-api-members/src/types.d.ts
| const filename = `beehiiv_api_members_${cursorIndex}.json`; | ||
|
|
||
| if (fileCache.hasFile(filename, 'tmp')) { | ||
| return await fileCache.readTmpJSONFile(filename); |
There was a problem hiding this comment.
Namespace the cache filename by pubId.
Within a shared tmp cache, page 0 for two different publications both map to beehiiv_api_members_0.json, so one run can replay another publication's cached payload. Include pubId in the key before reading/writing the cache file.
🔧 Minimal fix
- const filename = `beehiiv_api_members_${cursorIndex}.json`;
+ const filename = `beehiiv_api_members_${encodeURIComponent(pubId)}_${cursorIndex}.json`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const filename = `beehiiv_api_members_${cursorIndex}.json`; | |
| if (fileCache.hasFile(filename, 'tmp')) { | |
| return await fileCache.readTmpJSONFile(filename); | |
| const filename = `beehiiv_api_members_${encodeURIComponent(pubId)}_${cursorIndex}.json`; | |
| if (fileCache.hasFile(filename, 'tmp')) { | |
| return await fileCache.readTmpJSONFile(filename); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api-members/src/lib/fetch.ts` around lines 37 - 40, The
tmp cache filename is not namespaced by pubId so different publications collide;
update the filename construction in this function to include pubId (e.g., change
the filename variable used with fileCache.hasFile and fileCache.readTmpJSONFile
from `beehiiv_api_members_${cursorIndex}.json` to include pubId, such as
`${pubId}_beehiiv_api_members_${cursorIndex}.json`) so all fileCache operations
(hasFile/readTmpJSONFile and any corresponding writes) use the pubId-prefixed
key.
| if (!response.ok) { | ||
| throw new errors.InternalServerError({message: `Request failed: ${response.status} ${response.statusText}`}); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, verify the file exists and check the code around lines 10-11
cat -n packages/mg-beehiiv-api-members/src/lib/list-pubs.ts | head -20Repository: TryGhost/migrate
Length of output: 753
🏁 Script executed:
# Search for other InternalServerError usages to understand the pattern in this codebase
rg -A 3 "InternalServerError" --type ts packages/mg-beehiiv-api-members/Repository: TryGhost/migrate
Length of output: 1222
🏁 Script executed:
# Also check if there are other similar error patterns with context in the codebase
rg "errors\.(InternalServerError|BadRequest|ValidationError)" -A 5 --type ts | head -100Repository: TryGhost/migrate
Length of output: 7411
Add context object to the structured error.
The message alone loses which endpoint failed. Per coding guidelines, @tryghost/errors requires both message and context properties. Add a context object with the request URL/status metadata so CLI failures are diagnosable without leaking the API key.
🔧 Minimal fix
if (!response.ok) {
- throw new errors.InternalServerError({message: `Request failed: ${response.status} ${response.statusText}`});
+ throw new errors.InternalServerError({
+ message: `Request failed: ${response.status} ${response.statusText}`,
+ context: {
+ endpoint: url.toString(),
+ status: response.status,
+ statusText: response.statusText
+ }
+ });
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api-members/src/lib/list-pubs.ts` around lines 10 - 11,
The throw currently only sets a message; change it to pass a structured context
to errors.InternalServerError that includes the failing endpoint metadata (e.g.
response.status and response.statusText) and a non-sensitive form of the request
URL (use the response.url but strip query/credentials—e.g. new
URL(response.url).pathname or origin+pathname) so the context helps debugging
without leaking API keys; update the throw that uses errors.InternalServerError
to include both message and a context object containing { status:
response.status, statusText: response.statusText, path: <stripped-url-path> }
referencing the existing response and errors.InternalServerError symbols.
| const cachedFetch = async ({fileCache, key, pubId, limit = API_LIMIT, page}: {fileCache: any, key: string, pubId: string, limit?: number, page: number}) => { | ||
| let filename = `beehiiv_api_${limit}_${page}.json`; |
There was a problem hiding this comment.
Include pubId in the cache key.
beehiiv_api_${limit}_${page}.json collides across publications, so reusing the same cache directory for a different Beehiiv publication can replay another publication's cached page and silently export the wrong posts.
🧩 Suggested change
- let filename = `beehiiv_api_${limit}_${page}.json`;
+ let filename = `beehiiv_api_${pubId}_${limit}_${page}.json`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const cachedFetch = async ({fileCache, key, pubId, limit = API_LIMIT, page}: {fileCache: any, key: string, pubId: string, limit?: number, page: number}) => { | |
| let filename = `beehiiv_api_${limit}_${page}.json`; | |
| const cachedFetch = async ({fileCache, key, pubId, limit = API_LIMIT, page}: {fileCache: any, key: string, pubId: string, limit?: number, page: number}) => { | |
| let filename = `beehiiv_api_${pubId}_${limit}_${page}.json`; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api/src/lib/fetch.ts` around lines 29 - 30, The cache
filename in cachedFetch currently uses only limit and page and can collide
across publications; update the filename generation in cachedFetch (function
cachedFetch) to incorporate pubId (e.g., include pubId in the template for
filename or key) so cached files are namespaced by publication; ensure any pubId
values are safely encoded/sanitized when constructing the filename so fileCache
lookups and writes use the new pub-scoped key and avoid cross-publication cache
replay.
| if (!response.ok) { | ||
| throw new errors.InternalServerError({message: `Request failed: ${response.status} ${response.statusText}`}); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine the file
find . -type f -name "list-pubs.ts" | head -20Repository: TryGhost/migrate
Length of output: 161
🏁 Script executed:
cat -n ./packages/mg-beehiiv-api/src/lib/list-pubs.tsRepository: TryGhost/migrate
Length of output: 763
Add context property to structured error for diagnostics.
The error must include a context object alongside the message to comply with the @tryghost/errors structured error handling requirement. This provides request context (endpoint URL and HTTP status) for debugging failed API calls.
🔧 Minimal fix
if (!response.ok) {
- throw new errors.InternalServerError({message: `Request failed: ${response.status} ${response.statusText}`});
+ throw new errors.InternalServerError({
+ message: `Request failed: ${response.status} ${response.statusText}`,
+ context: {
+ endpoint: url.toString(),
+ status: response.status,
+ statusText: response.statusText
+ }
+ });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!response.ok) { | |
| throw new errors.InternalServerError({message: `Request failed: ${response.status} ${response.statusText}`}); | |
| if (!response.ok) { | |
| throw new errors.InternalServerError({ | |
| message: `Request failed: ${response.status} ${response.statusText}`, | |
| context: { | |
| endpoint: url.toString(), | |
| status: response.status, | |
| statusText: response.statusText | |
| } | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mg-beehiiv-api/src/lib/list-pubs.ts` around lines 10 - 11, The
thrown errors.InternalServerError lacks a structured context field; update the
error creation in list-pubs.ts where response is checked (the response.ok block)
to include a context object with the request endpoint and HTTP status (use
response.url and response.status) alongside the message so the thrown new
errors.InternalServerError contains both message and context for diagnostics.
| task: async (ctx, task) => { | ||
| ctx.options = options; | ||
| ctx.allowScrape = { | ||
| all: ctx.options.scrape.includes('all'), | ||
| assets: ctx.options.scrape.includes('all') || ctx.options.scrape.includes('assets') || ctx.options.scrape.includes('img') || ctx.options.scrape.includes('media') || ctx.options.scrape.includes('files'), |
There was a problem hiding this comment.
Initialize ctx.errors/ctx.warnings in the workspace task.
The CLI wrapper seeds these arrays, but getTaskRunner() is exported publicly. A direct caller that runs with a bare context will hit ctx.errors.push(...) in the first catch and mask the real failure with a TypeError.
💡 Proposed fix
task: async (ctx, task) => {
+ ctx.errors = ctx.errors || [];
+ ctx.warnings = ctx.warnings || [];
ctx.options = options;
ctx.allowScrape = {
all: ctx.options.scrape.includes('all'),Also applies to: 55-57
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/migrate/sources/beehiiv-api.js` around lines 14 - 18, The workspace
task's async handler (task: async (ctx, task) => { ... }) assumes ctx.errors and
ctx.warnings exist; initialize ctx.errors = ctx.errors || [] and ctx.warnings =
ctx.warnings || [] at the start of that function (and likewise in the analogous
block around lines 55-57) so direct callers using getTaskRunner() won't hit
TypeError when pushing into those arrays; update the task function(s) in
packages/migrate/sources/beehiiv-api.js to defensively create these arrays if
missing.
| } catch (error) { | ||
| ctx.errors.push({message: 'Failed to fetch content from beehiiv API', error}); | ||
| throw error; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
sed -n '45,185p' packages/migrate/sources/beehiiv-api.js
echo
rg -n -C2 "@tryghost/errors|ctx\.errors\.push\(" packages/migrate packages -g '!**/node_modules/**'Repository: TryGhost/migrate
Length of output: 50373
🏁 Script executed:
#!/bin/bash
# Check beehiiv-api.js imports and dependencies
head -20 packages/migrate/sources/beehiiv-api.js
echo "---"
# Check if `@tryghost/errors` is in migrate package.json
cat packages/migrate/package.json | grep -A 20 '"dependencies"'
echo "---"
# Find actual `@tryghost/errors` usage in codebase
rg -n "import.*@tryghost/errors|const.*errors.*=.*require.*@tryghost/errors" packages/migrate -B2 -A5Repository: TryGhost/migrate
Length of output: 1960
🏁 Script executed:
#!/bin/bash
# Get full package.json for migrate
cat packages/migrate/package.json
echo "---"
# Search entire codebase for `@tryghost/errors` usage examples
rg "@tryghost/errors" --type json --type jsRepository: TryGhost/migrate
Length of output: 5549
🏁 Script executed:
#!/bin/bash
# Look at actual `@tryghost/errors` usage patterns in sub-packages
rg -A5 "import errors from '@tryghost/errors'" packages/mg-substack/lib/process.js
echo "---"
# Look for error throwing pattern
rg -B2 -A3 "throw new errors\." packages/ -m 3
echo "---"
# Check if there's documentation or examples of `@tryghost/errors` usage
rg "new errors\." packages/mg-fs-utils/lib/FileCache.js -B2 -A2Repository: TryGhost/migrate
Length of output: 15214
Add @tryghost/errors dependency and refactor error handling to match repository standards.
The error objects being pushed to ctx.errors use a plain object shape {message: '...', error} instead of the repository standard @tryghost/errors structured errors. Add @tryghost/errors to packages/migrate/package.json dependencies and wrap errors in new errors.InternalServerError({message: '...', error}) at lines 55-57, 68-69, 80-81, 92-93, 126-127, 138-139, 170-171, and 182-183.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/migrate/sources/beehiiv-api.js` around lines 55 - 57, Add the
`@tryghost/errors` dependency to packages/migrate/package.json and import it into
packages/migrate/sources/beehiiv-api.js (e.g., const errors =
require('@tryghost/errors')); then replace all places that push plain objects
into ctx.errors (uses of ctx.errors.push({...})) with wrapped
repository-standard errors: ctx.errors.push(new
errors.InternalServerError({message: 'Failed to fetch content from beehiiv API',
error})); apply the same wrapping for each occurrence referenced (the other
messages) so every ctx.errors entry uses new
errors.InternalServerError({message: '...', error}) instead of a plain object.
WIP